在看 observer 的时候,在它的 constructor 里面是对数组有特殊的响应式处理的,下面讲解 Vue 中有关数组的响应式处理。
在 observer 中通过 Array.isArray 这个方法判断当前处理的数据是否是一个数组,如果是数组进行数组相关处理。
constructor (value: any) {
this.value = value
this.dep = new Dep()
// 初始化实例的 vmCount 为 0
this.vmCount = 0
// 将实例挂载到观察对象的 __ob__ 属性
def(value, '__ob__', this)
// 数组的响应式处理
if (Array.isArray(value)) {
// 浏览器兼容处理,判断浏览器是否支持对象的原型 __proto__ 属性
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 为数组的每一个对象创建一个 observer 实例
this.observeArray(value)
} else {
// 遍历对象中的每一个属性,转换成 setter/getter
this.walk(value)
}
}
如果 value 是一个数组,通过下面的方法判断浏览器是否支持对象的原型对象,然后根据支持的不同,选择给数组实现响应式的方法。
// can we use __proto__?
// 判断当前执行环境是否支持对象的原型对象属性
export const hasProto = '__proto__' in {}
如果判断当前执行环境是支持 proto 这个属性的,执行 protoAugment(value, arrayMethods)
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
protoAugment 这个方法的作用很简单,就是将第一个参数的原型对象重写为 src 。具体到这里就是给当前的数组属性的原型对象属性改为 arrayMethods。
下面看传入的第二个参数 arrayMethods 如何做处理的。
const arrayProto = Array.prototype
// 使用数组的原型创建一个新的对象,对象的原型指向数组的原型对象
export const arrayMethods = Object.create(arrayProto)
// 修改数组元素的方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
// 保存数组的原始方法
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
// 执行数组的原始方法
const result = original.apply(this, args)
// 获取数组对象的 ob 对象
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)
// notify change
// 调用了修改数组的方法,调用数组的 ob 对象发送通知
ob.dep.notify()
return result
})
})
可以看到 arrayMethods 是一个对象,这个对象集成了 Array 的原型,并且对 Array 原型上改变数组的实例方法进行了重写,使其在保证原有功能的基础上,还能自动的发送通知,更新视图。
如果判断当前的执行环境不支持 proto 这个属性,调用 copyAugment 这个方法,对数组进行响应式处理。
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
调用 copyAugment 这个方法传入的参数 arrayKeys 是一个通过 Object.getOwnPropertyNames 这个方法获取的 arrayMethods 的所有的属性 key 数组。
看 copyAugment 中的逻辑其实就是使用最简单粗暴的方式,直接通过循环的方式,将需要重写的数组的原型方法,给他们重新赋值更新,使其具备我们想要让他们有的功能。
最后执行的方法是 observeArray()
/**
* Observe a list of Array items.
*/
// 对数组做响应式的处理
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
observeArray 就是通过循环将数组中的每一个元素都进行响应式的处理,转换成 setter/getter。在 observe 里面会对元素的类型进行判断如果是数组或者对象进行处理,否则不处理。
<div id="app">
{{ arr }}
div>
<script src="../../dist/vue.js">script>
<script>
const vm = new Vue({
el: '#app',
data: {
arr: [2, 3, 5]
}
});
// vm.arr.push(8)
// vm.arr[0] = 100
// vm.arr.length = 0
script>
对于上述的示例,只有执行 vm.arr.push(8) 才会触发视图的自动更新,原因就是在 Vue 对数组的响应式处理中,只是对更改数组的方法进行了重写,示器具备了自动发送通知的功能,而数组对象的属性并没有全部重写,所以调用他的 length 这些属性的更改不会触发视图的自动更新。
如果我们想修改数组中的某一个值,可以调用数组的 splice 这个方法,这个方法可以实现将数组中的某几个元素替换成我们想要的元素。并且这个方法在数组响应式处理中是做过特殊处理的,所以可以通过 dep 的 notify 方法去发送通知,调用 watcher 的 update 方更新视图。