如果你阅读过Vue3源码,一定知道Vue3是通过ES6新增的Proxy对象实现了响应式机制,今天我们就来聊一聊Vue3是如何代理数组实现响应式的。在JavaScript中万物皆对象,数组也是一个特殊的对象,数组除了[[DefineOwnProperty]]内部方法与常规对象不同外其他内部方法都一样,因此当实现对数组的代理时,大部分代理对象的代码都可以继续使用:
const arr = reactive(['foo'])
effect(() => {
console.log(arr[0])
})
arr[0] = 'bar'
当我们通过arr[0]读取和修改数组项时,get/set拦截函数也会执行,所以上面这段代码会按照预期执行。但是数组的读取和设置操作不仅仅有这些,还有arr.length,for...in,for...of,includes,push等方法,我们需要对这些操作正确地建立响应式联系或触发依赖。
首先我们来看对数组索引和length的操作,观察如下代码:
const arr = reactive(['foo'])
effect(() => {
console.log(arr[0])
})
arr[1] = 'bar'
我们通过索引设置数组元素与设置对象属性仍然存在根本上的不同,因为数组对象部署的内部方法[[DefineOwnProperty]]规定:如果设置的索引值大于数组当前长度,那么要更新数组的length属性,所以当我们通过索引值设置元素时,可能会隐式地修改数组的length属性,因此在触发响应时,也应当触发与length相关的副作用,我们修改set拦截函数:
const p = new Proxy(obj,{
set(target,key,newVal,receiver){
const type = Array.isArray(target)
? Number(key) < target.length ? TriggerOpTypes.SET : TriggerOpTypes.ADD
: Object.prototype.hasOwnProperty.call(target, key) ? TriggerOpTypes.SET : TriggerOpTypes.ADD
const res = Reflect.set(target, key, value)
trigger(target, key, type, value)
return res
}
})
如果target是数组,检查被设置的索引值是否小于数组长度,如果是则视为SET操作,因为他不会改变数组长度,否则是ADD操作,因为这会隐式地修改数组的length属性,接下来我们也要修改trigger函数,在target是数组且操作类型为ADD时,应该取出并执行那些与length相关的副作用函数:
export const trigger = (target, key, type: TriggerOpTypes, newValue: unknown) => {
//...
if (type === TriggerOpTypes.ADD) {
//如果target是数组且操作类型为ADD,应该取出并执行那些与length相关的副作用函数
if (Array.isArray(target)) {
deps.push(depsMap.get('length'))
} else {
deps.push(depsMap.get(ITERATE_KEY))
}
} else if (type === TriggerOpTypes.DELETE) {
deps.push(depsMap.get(ITERATE_KEY))
}
//...
}
同样的,当我们修改数组的length属性时也有可能隐式地影响数组元素:
const arr = reactive(['foo'])
effect(() => {
console.log(arr[0])
})
arr.length = 0
上面代码中我们在副作用中访问了数组第0个元素,接着将数组的length属性修改为0,这时候数组的长度变成了0,即所有的元素都被删除,所以副作用函数应当重新执行,但是目前我们没实现这点,所以接下来需要修改set拦截函数以及trigger:
function createSetter() {
return (target: any, key: string | symbol, value: any) => {
//...
trigger(target, key, type, value)
//...
}
}
首先我们要把更新后的value传递给trigger方法,之后继续修改trigger方法:
export const trigger = (target, key, type: TriggerOpTypes, newValue: unknown) => {
//...
if (Array.isArray(target) && key === 'length') {
//如果target是数组并且修改了数组的长度,对于索引大于等于新length的元素
//需要把他们关联的副作用函数取出来并执行
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newLength) {
deps.push(dep)
}
})
}
//...
}
如果target是数组并且修改了数组的长度,对于索引大于等于新length的元素,需要把他们关联的副作用函数取出来并执行,这样我们修改数组的length也能正确地触发依赖了。