在上一篇Vue双向数据绑定-Object篇说过,Vue中Array和Object的双向数据绑定是不同的。当使用数组的方法去改变数据,是触发不了setter
的,那就是无法通setter
去通知依赖了,那么Vue是怎样对数组的改变进行监控的呢?javascript
由于Array
的实现依然须要用到defineReactive
、Dep
、 Watcher
、Observer
,这些方法或类的已经在上一篇大体实现了,不了解的建议先看完Vue双向数据绑定-Object篇。java
开始以前仍是先来点基础知识吧数组
要监控数据的变化,是否是要先知道那些方法会改变原数组呢?具体以下浏览器
想要监听数组的变化,当被监测的数组调用上面所说的方法,再去通知数组的依赖是否是就实现了对数组的监控了呢?固然这里说的只是当前Vue 2.x实现的数组监测,当this.arr[0] = 3
这样去改变数组的时候依然是没办法监测获得的,这个的完善就要等Vue 3了。app
原型链这个知识点要讲明白,篇幅要挺长的,请自行查阅资料继承与原型链。这里简单放个例子体现函数
var arr = [1, 2, 3]
arr.push = function(item) {
arr[arr.length] = 'push';
}
arr.push(4);
console.log(arr); // 输出 [1, 2, 3, 'push', push: f]
复制代码
这时arr.push
调的是本身定义的方法而不是Array.prototype
上的push
方法。工具
拦截器其实就是Vue对数组的 ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
方法进行了重写,在执行Array.prototype
的原生方法前,先去作一些Vue须要它作的功能以后在执行。post
const arrayProto = Array.prototype; // 原生的 Array.prototype
const arrayMethods = Object.create(arrayProto); // 拷贝
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function(method) {
const original = arrayProto[method];
// 改写arrayMethods中的 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' 方法
Object.defineProperty(arrayMethods, method, {
value: function(...args) {
// TODO 在这里实现Vue须要作的功能
return original.apply(this, ...args);
},
enumerable: false,
writable: true,
configurable: true
})
})
复制代码
arrayMethods是咱们改写过的Array.prototype,那么在把数据变成响应式的时候,把数组的原型方法改为咱们本身实现的arrayMethods。当触发这些方法时,就能够去通知依赖数据发生改变了,进行依赖触发吧。ui
前面Object
篇是经过Observer
变成响应式数据的,那么就在Observer
加上数组的代码this
const hasProto = '_proto_' in {}; // 判断浏览器可否访问原型
class Observer {
constructor(value) {
this.value = value;
if (Array.isArray(value)) { // 数组处理
if (hasProto) { // 浏览器兼容原型 把 _proto_ 改写成本身的 arrayMethods
value._proto_ = arrayMethods;
} else { // 不支持就把方法赋值到数组的属性上
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i];
value[key] = arrayMethods[key];
}
}
} else { // 对象处理
this.walk(value);
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReact(obj, key, obj[key]);
})
}
}
复制代码
数组的依赖收集相对于对象的会复杂些。这里先举例子强调一下
{
arr: [1, 2]
}
复制代码
data
里有个数组arr
,当使用时经过this.arr
是否是一样也会触发到getter
,那么就能够在这里进行依赖收集,可是以前的依赖是收集在defineReactive
中的dep
实例中,拦截器访问不到函数内部的dep
就触发不了依赖。
Vue.js是把Array
的依赖收集在Observer
中
// 工具方法
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
// Observer
class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
// 把Observer实例邦到 value 的 _ob_ 属性下,那么每一个响应式数据都有 _ob_ 这个属性
// 拦截器就能够经过 _ob_ 找到依赖了
def(value, '_ob_', this)
.......
}
.......
}
复制代码
上面只实现了把依赖收集在哪这一步。前面也说到了Array
也是在getter
中收集的
// 工具方法
function observe(value, asRootData) { // 获取Observer实例
if (!isObject(value)) {
return;
}
let ob;
if (hasOwn(value, '_ob_') && value._ob_ instanceof Observer) { // 已经被监测直接返回
ob = value._ob_
} else { // 还没监测
ob = new Observer(value);
}
return ob;
}
// defineReactive
function defineReactive(data, key, val) {
let childOb = observe(value);
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
dep.depend();
if (childOb) { // 新增
childOb.dep.depend(); // 依赖收集
}
return val;
},
set: function(newVal) {
if (val === newVal) {
return;
}
dep.notify();
val = newVal;
}
})
}
复制代码
这里就是增长了代码把依赖收集到Observer
实例中的dep
,实际上加了这一步,每一个被监测的数据上都有_ob_
属性。这里留下个问题去思考下“为何要不直接都用childOb.dep
,还多用了一个dep
?”
上一节实现了依赖的收集,如今要在拦截器实现依赖触发了
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function(method) {
const original = arrayProto[method];
// 利用上面的工具方法 def 改写一下
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args);
const od = this._ob_; // 获取 Observer实例
ob.dep.notify(); // 触发依赖
return result;
});
})
复制代码
那么到如今这一步数组的监测已经算是完成了,但这仅仅是对数组自己的实现,数组里的子元素是对象和新增的元素一样也要把它变成响应式数据,接下来就完善这些操做。
应该都很快想到用递归去实现,继续改写Observer
class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
def(value, '_ob_', this);
if (Array.isArray(value)) {
if (hasProto) {
value._proto_ = arrayMethods;
} else {
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i];
value[key] = arrayMethods[key];
}
}
this.observeArray(value); // 递归监测数组的每一项
} else {
this.walk(value);
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReact(obj, key, obj[key]);
})
}
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]); // observe 工具方法
}
}
}
复制代码
在Observer
类上增长了observeArray
方法,这样就能够在拦截器上经过_ob_
来调用。
在上面说的数组方法中有push
、unshift
和splice
是能够添加元素的,就要在拦截器上对这些新增元素来让它变成响应式数据。
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].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) {
case: 'push':
case: 'unshift':
inserted = args;
break;
case: 'splice':
inserted = args.splice(2);
break;
}
if (inserted) {
ob.observeArray(inserted); // 监测新增元素
}
ob.dep.notify();
return result;
})
})
复制代码
到这里数组自己、数组子元素和新增数组元素的监测已经所有实现,原理的掌握并不难,就是依赖这部份内容相对对象的处理来讲复杂一点,也有点绕,看多几遍就能搞懂了。
Array
的监测经过getter
和拦截器来实现,在getter
中收集依赖,在拦截器中触发依赖。依赖收集在Observer
使拦截器可以调用。