在当中,我们剖析了对象的变化侦测。由于其侦测方式是通过 getter/setter 实现的,而当通过 array.push,array.pop 等方法操纵数组时,是不会触发 getter/setter 的。
问题一:如何追踪数组变化
和追踪对象类似,我们的需求是在调用 array.push 等函数时能够收到通知。Vue.js 中是通过创建一个拦截器覆盖 Array.prototype。之后,当调用数组方法时,执行的将会是拦截器中的提供方法,我们也就能因此追踪到数组的变化。下面是一个基础拦截器的实现:
const arrayProto = Array.prototypeconst arrayMethods = Object.create(arrayMethods);[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach((method) => { const original = arrayProto[method] Object.defineProperty(arrayMethods, method, { value: function mutator (...args) { return original.apply(this, args) }, enumerable: false, writable: true, configurable: true })})复制代码
在上面的代码中,我们创建了 arrayMethods 变量,它继承 Array.prototype。接着我们又在 arrayMethods 中使用了 Object.defineProperty。这样当我们调用 arrayMethods.push 时,其实是调用了其中的 mutator 函数,显然我们可以在 mutator 函数中添加对象变化侦测类似 setter 的逻辑。
我们不能直接让 arrayMethods 覆盖 Array.prototype,那会污染全局的 Array。我们的目的仅仅是拦截那些需要观测的数组,所以放在 Observer 类中处理会更为合理。
class Observer { constructor(value) { this.value = value if (Array.isArray(value)) { /** * 新增,注意不是所有浏览器都支持__proto__属性,Vue.js 源码在这段 * 逻辑里面做了兼容处理,若不支持,则直接遍历 arrayMethods * 将方法设置在被侦测的数组中 **/ value.__proto__ = arrayMethods } else { this.walk(value) } } ...}复制代码
问题二:如何收集依赖并向依赖发送通知
Object 的依赖是在 getter 中的使用 Dep 收集的,每个 key 都有一个对应的 Dep 列表来存储依赖。Array 的依赖和 Object 一样,也是在 defineReactive 中收集。
function defineReactive(data, key, val) { if (typeof val == 'object') { new Observer(val) } let dep = new Dep() Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { dep.depend() // 这里收集 Array 的依赖 return val }, set: function(newVal) { if (val === newVal) return val = newVal dep.notify() } })}复制代码
但此时我们需要改写一下 Dep 保存的地方,若按照之前的方式,对数组而言,我们只能在 getter 中访问到 dep,但在拦截器中是无法访问的,所以现在改写一下 Observer 类和 defineReactive:
class Observer { constructor(value) { this.value = value this.dep = new Dep() if (Array.isArray(value)) { value.__proto__ = arrayMethods } else { this.walk(value) } } ...}function defineReactive (data, key, val) { let childob = observe(val) // 修改 let dep = new Dep() Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { dep.depend() // 新增 if (childOb) { childOb.dep.depend() } // 这里收集 Array 的依赖 return val }, set: function(newVal) { if (val === newVal) return val = newVal dep.notify() } })}function observe (value, asRootData) { 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 中调用了 observe,它把 val 作为参数输入,并返回一个 Observer 实例。这样我们就可以通过调用 childOb.dep.depend() 在 getter 中添加依赖。接下来我们还需要修改一下 Observer 将 ob 属性到实例当中:
class Observer { constructor(value) { this.value = value this.dep = new Dep() // 新增 def(value, '__ob__', this) if (Array.isArray(value)) { value.__proto__ = arrayMethods } else { this.walk(value) } } ...}function def (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true })}复制代码
我们已经可以通过数组数据的 ob 属性拿到 Observer 实例,然后就可以拿到 ob 上的 dep。现在我们就可以在拦截器中拿到依赖了:
;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach((method) => { const original = arrayProto[method] def(arrayMethods, method, function mutator(...args) { const result = original.apply(this, args) const ob = this.__ob__ 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)) { this.observeArray(value) value.__proto__ = arrayMethods } else { this.walk(value) } } observeArray (items) { for (let item in items) { observe(item) } } ...}复制代码
问题三:如何侦听新增元素
当我们使用 push 或是 unshift 等方法添加元素时,新元素不是响应式的,所以我们要在拦截器中添加处理:
;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach((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.slice(2) break } if (inserted) ob.observeArray(inserted) ob.dep.notify() return result })})复制代码
问题四:Array 的遗留问题
- 当直接使用索引设置一个数组项时,比如 this.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:this.items.length = newLength 从上面的实现原理中可以判断,以上两种情况我们是没有办法监控到的。
Array 的变化侦测过程梳理
Array 是通过创建拦截器去覆盖数组原型的方式来追踪变化的,收集依赖的方式和 Object 一样,都是在 getter 中收集。但是由于依赖的使用位置不同,要在拦截器向依赖发送消息就必须能访问到依赖。所以依赖不能像 Object 那样保存在 defineReactive 中,而是把依赖保存在 Observer 实例上。所以我们在 Observer 实例中绑定了 ob 属性,并将 this 保存在 ob 上。