在上一篇文章当中,我们剖析了对象的变化侦测。由于其侦测方式是通过 getter/setter 实现的,而当通过 array.push,array.pop 等方法操纵数组时,是不会触发 getter/setter 的。
问题一:如何追踪数组变化
和追踪对象类似,我们的需求是在调用 array.push 等函数时能够收到通知。Vue.js 中是通过创建一个拦截器覆盖 Array.prototype。之后,当调用数组方法时,执行的将会是拦截器中的提供方法,我们也就能因此追踪到数组的变化。下面是一个基础拦截器的实现:
const arrayProto = Array.prototype
const 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 上。