关于Array和纯粹Object类型的响应式注册,Vue里面比较巧妙。
1、Array响应式注册源码解析
Array对象有push pop shift unshift reverse sort splice7种方法会改变原值。但是如何给这7种方法实现响应式机制呢?源码如下:
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator() {
var args = [], len = arguments.length;
while (len--) args[len] = arguments[len];
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
if (inserted) { ob.observeArray(inserted); }
// notify change
ob.dep.notify();
return result
});
});
- Array类型的对象收集依赖时很简单,get方法中实现即可。但是触发依赖并不能使用set方法,也不能使用a[0]=1这种方式,须使用相关API
- 所以在7种方法触发的时候,触发依赖就可以了
- Vue实现了对Array原型方法的重写,这可以避免对Array原型的修改(因为也不建议直接修改原型对象),同时也可以保留数组的其他属性
- 首先新建一个对象arrayMethods,该对象继承Array.prototype,所以arrayMethods就有了数组原型对象的所有属性
- 然后对arrayMethods的7种方法重写,使用Object.defineProperty,把方法赋值给arrayMethods,每次数组对象调用这7种方法时,就会调用mutator
- mutator方法,主要做了以下几件事情:
- 把7种方法绑定在arrayMethods,也就是original.apply(this, args)
- 若有新增元素,须对新增元素收集依赖,所以对push unshift splice特殊处理
- 最后触发变更,也就是ob.dep.notify()
- arrayMethods实则是一个原型对象,下一步只需要把arrayMethods赋值给数组对象的__proto__即可
- 原型对象绑定代码如下:
var Observer = function Observer(value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
如果数组对象没有__proto__,就把arrayMethods的每个方法赋值给该对象的原型,且这些方法是不可枚举的 如果数组对象存在原型,直接arrayMethods替换原型就可以了。
2、Object响应式注册源码解析
关于Object响应式注册机制,对于一般化的赋值,触发get和set方法即可。但是Object新增属性和删除属性是另一种实现方式,这里仅介绍这两种方式的源码:
function set(target, key, val) {
if (isUndef(target) || isPrimitive(target)
) {
warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
var ob = (target).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
);
return val
}
if (!ob) {
target[key] = val;
return val
}
defineReactive$$1(ob.value, key, val);
ob.dep.notify();
return val
}
function del(target, key) {
if (isUndef(target) || isPrimitive(target)
) {
warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target))));
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return
}
var ob = (target).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
);
return
}
if (!hasOwn(target, key)) {
return
}
delete target[key];
if (!ob) {
return
}
ob.dep.notify();
}
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
新增方法就是$set。
1、新增时,传入三个参数,分别是目标对象、新增属性名、属性值,如果属性名可以转换为合法的数字,且目标对象是数组,可以调用splice方法实现数组的新增;
2、如果新增属性是对象的已有属性,就赋值操作
3、后面这一段代码:
defineReactive$$1(ob.value, key, val);
4、如果新增了属性,必然要给新属性收集依赖,就是这个意思
5、最后触发依赖即可
删除方法就是$delete
1、删除,传入目标对象和属性两个参数,如果目标对象是数组且属性可以转换为索引,调用splice实现删除
2、如果该属性存在,调用delete删除对象属性
3、最后触发依赖即可