目录
Object通过setter改变属性的值,所以我们利用getter时发送依赖收集,在setter时触发依赖更新,而且Vue将数据转换成响应式数据是在数据初始化时,对Object中之后的属性新增和删除操作,无法做到自动更新,而是通过vm. s e t 和 v m . set和vm. set和vm.delete手动转换成响应式,并立即发出更新通知。
但是,一般在对数组的操作中,可以改变数组自身内容的方法有push、pop、shift、unshift、splice、sort、reverse七个。当我们给一个数组类型的属性赋值时,属性的setter函数会触发,从而通知更新。但是在使用push等一系列操作方法时,由于ES6之前,JS没有元编程能力,没有提供可以拦截原型方法的能力,所以,我们思考,如果能在用户使用这些方法操作数组时得到通知,那就达到了追踪的目的。
如何做到在操作这些原型方法时能得到通知呢?
对!就是拦截原型。
基本原理:用一个拦截器覆盖Array.prototype, 每当使用原型上的方法操作数组时,实际上执行的都是拦截器提供的方法,在拦截器中除了调用原生的方法操作数组外,还可以干点别的事,比如:通知依赖更新!
methodsToPatch
var arrayProto = Array.prototype;
// 创建一个新的空对象arrayMethods,并将原型指向Array.prototype
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsToPatch.forEach(function (method) {
// 缓存原始方法
var original = arrayProto[method];
// 方法重写,屏蔽了Array.prototype上的方法,同时内部调用Array.prototype上的原始方法
def(arrayMethods, method, function mutator (...args) {
var result = original.apply(this, args);
// 占位符D1 :这里可以做些事:比如通知依赖更新...
return result
});
});
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
拦截实质上运用了JS中 [[Prototype]]
机制,在对象自身上查不到属性和方法引用时,引擎就会继续在[[Prototype]]
关联的对象上进行查找,直到顶层Array.prototype
。
so! 我们可以在Array类型的数据上定义这些方法,从而拦截了使用Array.prototype
上的原生方法。然而为每个需要追踪的数据都添加这七个方法,实在是繁琐。但是,我们有了原型链查找这种思想,可以轻松的实现委托。
即:可以通过将数据的原型(可以通过__proto__
访问)直接关联到拦截器对象arrayMethods
上,实现拦截。
现在,我们访问某个数组(如:list
)的push方法时,它的查找顺序是:
list自身——>arrayMethods ——> Array.prototype
注意:
对于那些不支持__proto__
的浏览器,我们只能手动的为每个数据添加方法了。
const hasProto = '__proto__' in {}
const arrayKey = Object.getOwnPropertyNames(arrayMethods)
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
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);
}
};
function protoAugment (target, src) {
target.__proto__ = src;
}
function copyAugment (target, src, keys) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
def(target, key, src[key]);
}
}
再说明this.observeArray(value);
做了啥之前,我们先搞清楚如何收集跟数据相关的依赖(也就是视图中对应的坑{{}}
)以及操作数据时是如何通知依赖的呢?
先看下我们访问一个数组的形式,如:
this.list
这一定会触发list
的getter
属性
所以我们依然在getter中收集数组依赖,在拦截器中触发依赖
Object侦测中,依赖的收集、存放、通知都集中在defineReactive函数中。然而,数组通知依赖必须在拦截器中实现,所以,我们将收集的依赖集合放在Observer中,并将每一个数据实例化的Observer挂载到它的__ob__
属性上。
在拦截器中,通过this.__ob__.dep
就可以访问所有的依赖。
// this 指向Observer实例对象
this.dep = new Dep();
def(value, '__ob__', this);
__bo__
的作用:
同理,我们通过拿到数据的Observer实例,进而拿到dep
列表,进行依赖存储。
在getter中收集依赖
便于理解,我们举个例子:
{
name: '',
obj: {
list: [...]
}
}
// key: list val: [...]
function defineReactive (obj, key, val) {
var dep = new Dep(); // 用于Object类型的数据
// 1. 返回val的Observer实例,可以拿到dep列表
var childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
if (Dep.target) {
// Object的依赖收集
dep.depend();
//2. Array的依赖收集
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function (newVal) {
if(val === newVal) return
val = newVal;
observe(newVal);
dep.notify();
}
});
}
function dependArray (value) {
for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
e = value[i];
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
dependArray(e);
}
}
}
function observe (value) {
if (!isObject(value)) {
return
}
var ob;
// 判重 如果value具有'__ob__'属性,说明已经是响应式数据
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
return ob
}
通过前面的赘述,在拦截器中我们可拿到数据的this.__ob__.dep
在拦截器方法重写代码中的占位符D1处添加依赖通知:
var ob = this.__ob__;
ob.dep.notify() //向依赖发送消息
除了对数组本身的操作进行追踪外,还要对数组的子集(如 ["a", {name: 'xxx'}, 3]
)以及通过push、unshift、splice操作新增的数组元素进行响应式绑定。
还记得之前那个this.observeArray(value)
吗?对,它的作用就是循环数组中的每一项,执行 observer函数,来侦测变化。
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
observe 会将数据转换成响应式的。其实是递归调用了new Observer()
思路:拿到新增的元素,并使用Observer侦测它
在方法调用函数中来拿到新增元素,还记得拦截器方法重写代码中的占位符D1吗?在这里添加:
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);
}
同理,拿到新增元素后,调用observeArray方法进行响应式绑定。
正式由于Array的响应式是通过拦截原型方式实现的,所以对于数组的某些操作,Vue是拦截不到的。
例如:
this.list[0] = 2
this.list.length = 0
在未来的Vue中,可能的解决方案是Proxy。