前面我们已经说过Object的检测方式,为啥还要单独把Array拎出来说呢?
因为Object.defineProperty无法准确检测到数组的变化,如我们使用push给一个数组添加了元素并不会触发setter和getter方法。
# 如何追踪变化
我们可以用一个拦截器覆盖Array.prototype。然后每当使用Array原型上的方法操作数组时,执行的都是这个拦截器中的方法,在拦截器中使用原生Array的原型方法去操作数组。
# 拦截器
const arrayProto = Array.prototype
// 此处的分号不可省略,再编写JS代码时并非所有的语句后面都可以省略分号
const arrayMethods = Object.create(arrayProto);
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(function(method) {
// 缓存原始方法
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
vaule: function mutator (...args){
return original.apply(this, args)
},
enumerable: false,
writable: true,
configurable: true
})
})
# 使用拦截器覆盖Array原型
在Observer中处理要被转成响应式的数组类型
export class Observer {
constructor(value) {
this.value = value
if (Array.isArray(value)) {
+ value.__proto__ = arrayMethods
}else {
this.walk(value)
}
}
}
使用__proto__覆盖原型:

可以使用Object.setPrototypeOf来代替__proto__实现同样的效果。
# 将拦截器方法挂载到数组的属性上
上面的代码有个很大问题,并非所有浏览器都支持__proto__属性,Vue中如果浏览器中支持__proto__就直接用__proto__,如果浏览器不支持__proto__就将arrayMethods身上的方法设置到被侦测的数组上
import { arrayMethods } from './array'
const hasProto = '__proto__' in {}
// Object.getOwnPropertyNames()方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
const arrayKeys = Object.getOwnPropertyName(arrayMethods)
export class Observer {
constructor(value) {
this.value = value
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
}else {
this.walk(value)
}
}
...
}
function protoAugment(target, src, keys) {
target.__proto__ = src
}
function copyAugment(target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}

# 如何收集依赖
通过前面的学习我们知道Object是在defineReactive中的getter中通过Dep收集依赖的,那么数组应该怎么收集依赖呢?
实际数组也是在getter中收集依赖的,因为Vue中的数据都定义在data这种属性中,它是一个对象(即便是一个方法,返回的也是一个对象)
{
data() {
return {
arr: [1, 2, 3]
}
},
create() {
console.log(this.arr) // 访问数组
}
}
在我们通过this.arr访问数组时首先会触发arr属性的getter方法,我们可以在这里收集数组依赖
function defineReactive(data, key, val) {
let dep = new Dep()
Object.defineProperty(data, key, {
get() {
dep.depend()
// 在这里收集Array的依赖
return val
}
})
}
Array在getter中收集依赖,在拦截器中触发依赖
# 依赖列表存在哪儿
数组依赖我们应该存放到哪呢?对数组依赖的存放相对麻烦些,因为我们需要在getter中收集依赖,在数组拦截器中触发依赖,然而这两个文件并不在一块,我们需要找个这两个文件都能访问的地方来存放依赖。
在Vue源码中把Array的依赖存放在Observer中,因为在getter和数组拦截器中都能访问到Observer的实例
export class Observer {
constructor(value) {
this.value = value
+ this.dep = new Dep()
...
}
...
}
# 收集依赖
function defineReactive(data, key, val) {
let childOb = observe(val)
let dep = new Dep()
Object.defineProperty(data, key, {
get() {
if (childOb) {
childOb.dep.depend()
}
return val
}
...
})
...
}
export 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
}
observe方法尝试为value创建一个Observer实例,如果创建成功,直接返回新创建的Observer实例。如果value已经存在一个Observer实例,则直接返回它,避免了重复侦测value的变化
在defineReactive中调用了observe方法,如果传入的参数是一个对象或者数组会得到一个Observer的实例childOb,调用childOb的属性下的depend方法收集依赖。
# 在拦截器中获取Observer实例
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
export class Observer {
constructor(value) {
...
def(value, '__ob__', this)
...
}
...
}
// 数组拦截器
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// 缓存原始方法
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
value: function mutator (...args) {
const ob = this.__ob__ // 新增
return original.apply(this, args)
},
enumerable: false,
writable: true,
configurable: true
})
})
在Observer中为vaule新增了一个不可枚举的属性__ob__值为Observer的实例。如果value是数组的话,当我们调用改变数组方法的时候如value.push(1)就会走到数组拦截器,此时数组拦截器中的this指的就是value,就可以在数组拦截器中通过this.__ob__取得observer的实例
# 向数组的依赖发送通知
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// 缓存原始方法
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
value: function mutator (...args) {
const ob = this.__ob__
ob.dep.notify() // 向依赖发送消息
return original.apply(this, args)
},
enumerable: false,
writable: true,
configurable: true
})
})
通过前面的操作,我们在数组拦截器中获取到了Observer的实例,就可以调用dep.notify()方法通知依赖发生了变化。
# 侦测数组中元素的变化
回忆下前面代码中的Observer方法中我们只对对象进行了递归侦测,并未对数组中的元素进行处理,下面我们来改进下
export class Observer {
constructor (value) {
this.value = value
def(value, '__ob__', this)
// 新增
if (Array.isArray(value)) {
this.observeArray(value)
} else {
this.walk(value)
}
}
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
……
}
新增observeArray方法,用于侦测数组中的每一项
# 侦测新增元素的变化
上面我们对数组中原有元素的变化进行了侦测,但是对于新增元素如何进行侦测呢?
首先我们要获取到哪些是新增的元素,然后对这些新增的元素增加侦测。
;[
'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.slice(2)
break
}
ob.dep.notify()
return result
})
})
在数组拦截器中我们通过switch...case对增加数组元素的方法进行了处理,将新增的元素存放到了inserted变量中,完成了获取新增元素的任务,下面来使用Observer侦测新增元素。
回想下,前面我们对数组元素进行侦测使用的是observeArray方法,对于新增方法是否也可以使用这个方法呢?
答案当然是可以的,我们在数组拦截器中可以通过this.__ob__获得Observer的实例,因此可以访问到observeArray方法,如果有新增元素的话直接调用observeArray方法对新增元素进行侦测即可。
;[
'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.slice(2)
break
}
if (inserted) ob.observeArray(inserted) // 新增
ob.dep.notify()
return result
})
})
# 关于Array的问题
Vue对数组的变化侦测是通过拦截原型方法的方式实现的,因此一些非原型方法的操作就拦截不到。如:
this.list[0] = 1
this.list.length = 0
上面这样修改数组元素值和清空数组的操作Vue就侦测不到,也就没办法响应。因为在ES6之前无法做到模拟数组的原生行为。ES6提供了元编程的能力有能力拦截,所以Vue3使用ES6提供的Proxy来实现这个功能。
# 总结
Array追踪变化的方式和Object不一样。因为它是通过方法来改变内容的,所以我们通过创建拦截器去覆盖数组原型的方式来追踪变化。
为了不污染全局Array.prototype,我们在Observer中只针对那些需要侦测变化的数组使用 proto 来覆盖原型方法,但 proto 在ES6之前并不是标准属性,不是所有浏览器都支持它。因此,针对不支持 proto 属性的浏览器,我们直接循环拦截器,把拦截器中的方法直接设置到数组身上来拦截Array.prototype上的原生方法。
Array收集依赖的方式和Object一样,都是在getter中收集。但是由于使用依赖的位置不同,数组要在拦截器中向依赖发消息,所以依赖不能像Object那样保存在defineReactive中,而是把依赖保存在了Observer实例上。
在Observer中,我们对每个侦测了变化的数据都标上印记 ob,并把this(Observer实例)保存在 ob 上。这主要有两个作用,一方面是为了标记数据是否被侦测了变化(保证同一个数据只被侦测一次),另一方面可以很方便地通过数据取到 ob,从而拿到Observer实例上保存的依赖。当拦截到数组发生变化时,向依赖发送通知。
除了侦测数组自身的变化外,数组中元素发生的变化也要侦测。我们在Observer中判断如果当前被侦测的数据是数组,则调用observeArray方法将数组中的每一个元素都转换成响应式的并侦测变化。
除了侦测已有数据外,当用户使用push等方法向数组中新增数据时,新增的数据也要进行变化侦测。我们使用当前操作数组的方法来进行判断,如果是push、unshift和splice方法,则从参数中将新增数据提取出来,然后使用observeArray对新增数据进行变化侦测。
由于在ES6之前,JavaScript并没有提供元编程的能力,所以对于数组类型的数据,一些语法无法追踪到变化,只能拦截原型上的方法,而无法拦截数组特有的语法,例如使用length清空数组的操作就无法拦截。