开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情
数组检测
通过 Object.defineProperty 方法重写 getter setter 的方式来实现数据响应式,无法监听到数组插入删除的变化,这也是使用 Object.defineProperty 方法进行数据监控的缺陷。在 Vue 源码中,通过重写数组方法的方式解决了这一问题。来看下具体实现
数组方法重写
Vue 在保留原数组功能的前提下,对数组进行额外的操作,也就重新定义了数组方法。
// 首先将数组原型保存下来,需要重写的方法都在数组原型上
const arrayProto = Array.prototype
// 创建一个继承数组原型的对象,该对象拥有数组原型上的所有方法。
export const arrayMethods = Object.create(arrayProto)
// 需要重写的数组方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
接下来,对 methodsToPatch 中列举的方法进行重写。
methodsToPatch.forEach(function (method) {
// cache original 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)
// notify change
ob.dep.notify()
return result
})
})
function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
重写数组方法的基本思路,就是创建一个继承数组原型的对象,这样该对象就拥有了数组原型上的所有方法,然后对该对象的方法进行重写, 在重写的方法内部加上我们需要进行的额外才做,并调用数组原型上原本的方法。这样就达到了扩展方法的目的,能够对数组的增删进行拦截。
当我们在执行数据方法添加或删除数据元素时,就会触发设置 mutator 方法。
为什么需要创建一个继承数组原型的对象? 因为这样才不会污染全局的数组原型,我们只需要对 Vue 实例的 data 选项中的数组进行拦截。
现在数组方法已经重写完成了,那么在我们访问数组方法时,如何才能不调用原生的数组方法, 而是调用我们重写的方法呢?
回到数组初始化的过程中,在实例化 Observer 时,看下对于 Array 类型的数据时怎么处理的。
class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
}
对数组的处理存在两个分支, hasProto 作为判断条件, 用来判断当前运行的环境,在对象的原型上是否存在 __proto__ 属性。来看两个分支分别对应执行的方法
function protoAugment (target, src: Object) {
target.__proto__ = src
}
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])
}
}
当对象原型上存在 __proto__ 属性时,直接将我们重写方法后的对象赋值给数组原型;如果没有 __proto__ 属性,则通过代理的方式在数组上添加方法。
在执行完这一步之后,当我们访问数组方式,调用的就是我们重写的方法了。
依赖收集
在数据初始化阶段会利用 Object.defineProperty 进行数据访问的改写, 也就是数据响应式化。在访问数组元素时,同样回个 getter 方法所拦截。而对于数组,在数据拦截的过程中需要进行特殊处理,在 defineReactive 方法中,对于数组类型的数据,会调用 defineArray 方法实现数据拦截操作
function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
}
})
}
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
dependArray 方法中会对数组中每一个元素进行依赖收集,如果数组中元素是数组类型,则进行递归处理
派发更新
当调用数组的方法添加或者删除元素是, setter 方法是无法进行拦截的,唯一可以进行拦截的地方就是之前我们重写的各种数组方法,
methodsToPatch.forEach(function (method) {
// cache original 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)
// notify change
ob.dep.notify()
return result
})
})
在重写的数组房中,取出数组中的 __ob__ 属性,也就是 Observer 实例,调用 ob.dep.notify 方法,进行依赖的派发更新。 如果调用的方法往数组中添加了元素,则需要对新添加的元素进行响应式化。
class Observer{
// 对新添加的数组元素进行响应化
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
总结
在 Vue 中,数组的增删无法通过 setter 进行依赖更新,所以在 Vue 中,重新定义了数组的常用方法。同时在访问数组元素是依旧触发 getter 方法来进行依赖收集;在改变数组时,通过触发重写的数组方法运行,进行依赖的派发更新