前面的话
前端日问,巩固基础,不打烊!!!
解答
首先:Object.defineProperty
是可以对数组进行劫持的,但对后来新添加的属性是不会劫持的。
也就是说,Object.defineProperty
是根据数组的索引来监听数组的变化的,只不过不能劫持后添加的属性。
来看例子:
function defineReactive(data,key,value){
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
console.log(`get key: ${key} value: ${value}`);
return value;
},
set: function(newValue){
console.log(`set key: ${key} value: ${newValue}`);
value = newValue;
}
})
}
function observe (data) {
Object.keys(data).forEach(function(key){
defineReactive(data,key,data[key]);
})
}
let arr = [1, 2, 3];
observe(arr);
这说明Object.defineProperty
是可以通过数组的索引来监听数组的变化 。
我们接着看,给数组添加一个值会怎样:
所添加的值,并没有触发上面的setter
或getter
方法,原因就是并没有对新添加的下标进行observer
。
总结一下:
Object.defineProperty
对数组的劫持与对象一样,会把数组的索引当作key
来监听数组,只是只能监听初始的索引变化。如果使用push
或shift
来增加索引,需要再手动初始化才能被observer
。
Observer源码如何实现
在源码上,Observer
对数组进行了单独的处理
-
首先来看看
hadProto
这个常量,定义如下:export const hasProto = '__proto__' in {}
简单来说,就是用来表示浏览器是否支持直接使用
__proto__
,一个布尔值。即如果浏览器支持隐式的原型
__proro__
,则调用protoAugment
方法,否则调用copyAugment
方法。 -
下面看一下
protoAugment
方法function protoAugment (target, src: Object, keys: any) { /* eslint-disable no-proto */ target.__proto__ = src }
直接将数组的实例通过
__proto__
与arrayMethods
对象连接起来。从而继承了arrayMethods上的方法。 -
接着看
arrayMethods
是如何定义的:// 缓存数组原型 const arrayProto = Array.prototype; // 实现 arrayMethods.__proto__ === Array.prototype export const arrayMethods = Object.create(arrayProto); // 需要进行功能拓展的方法 const methodsToPatch = [ "push", "pop", "shift", "unshift", "splice", "sort", "reverse" ]; /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function(method) { // 缓存原生数组方法 const original = arrayProto[method]; def(arrayMethods, method, function mutator(...args) { // 执行并缓存原生数组功能 const result = original.apply(this, args); // 响应式处理 const ob = this.__ob__; let inserted; switch (method) { // push、unshift会新增索引,所以要手动observer case "push": case "unshift": inserted = args; break; // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。 case "splice": inserted = args.slice(2); break; } // if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听 // notify change ob.dep.notify();// 通知依赖更新 // 返回原生数组方法的执行结果 return result; }); }); };
arrayMethods 是 原生数组的一个实例,然后遍历
methodsToPatch
数组,对每一个元素进行def
。def方法的作用,如果不深究的话,可以粗略的表示为:
arrayMethods[method] = function mutator(){};
如果是:
push、unshfit、splice
方法添加了新的元素,就要将添加的元素Observer
一次,使其变为响应式。
下面附上def与observerArray方法的源码:
-
def 方法源码:
对
methodsToPatch
中的每一个方法(push、shfit等)进行数据劫持,并且挂载在arrayMethods
对象上。export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
-
observerArray方法源码
对数组的每一个元素进行监听,使其变为响应式。
Observer.prototype.observeArray = function observeArray(items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); }
小结:
总的来说,就是重写了原生的方法,通过target__proto__ == arrayMethods
来改变了数组实例的原型。arrayMethods
上的每一个属性(即push、shfit)都被重写了。
注意:改变了原型链,target__proto__==arrayMethods;arrayMethods.proto==Array.Prototype,通过改变原型链来进行数组方法的重写。
Object.defineProperty VS Proxy
-
Object.defineProperty只能劫持对象的属性(需要遍历),而Proxy是直接代理对象。
-
Object.defineProperty对新增属性需要手动进行Observe。
也正是这个原因,Vue给data中的数组或者对象新增属性时,需要使用vm.$set
才能保证新增属性也是响应式的。附上set的源码:
/** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */ export function set (target: Array<any> | Object, key: any, val: any): any { // 如果target是数组,且key是有效的数组索引,会调用数组的splice方法, // 我们上面说过,数组的splice方法会被重写,重写的方法中会手动Observe // 所以vue的set方法,对于数组,就是直接调用重写splice方法 if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } // 对于对象,如果key本来就是对象中的属性,直接修改值就可以触发更新 if (key in target && !(key in Object.prototype)) { target[key] = val return val } // vue的响应式对象中都会添加了__ob__属性,所以可以根据是否有__ob__属性判断是否为响应式对象 const ob = (target: any).__ob__ // 如果不是响应式对象,直接赋值 if (!ob) { target[key] = val return val } // 调用defineReactive给数据添加了 getter 和 setter, // 所以vue的set方法,对于响应式的对象,就会调用defineReactive重新定义响应式对象,defineReactive 函数 defineReactive(ob.value, key, val) ob.dep.notify() return val }
在 set 方法中,对 target 是数组和对象做了分别的处理,target 是数组时,会调用重写过的 splice 方法进行手动 Observe 。
对于对象,如果 key 本来就是对象的属性,则直接修改值触发更新;否则调用 defineReactive 方法重新定义响应式对象。
-
Proxy 是直接可以通过
set(target, propKey, value, receiver)
拦截对象属性设置,是可以拦截到对象的新增属性的。 -
Proxy支持13种拦截操作,这是defineProperty所不具有的
参考文章:
转载:https://blog.csdn.net/qq_41257129/article/details/105196145