对面试Vue的小伙伴来说,Vue的响应式原理是备考项,而且相信大家都能脱口而出两个API:Object.defineProperty(Vue2)和Proxy(Vue3)。
当一个理论已经成为常识的阶段时,就不得不往下卷了。
那么问题来了:Vue3为什么要改用Proxy?
肯定是更好呗!那好在哪里呢?解决了什么问题呢?
我们先来看一段代码:
js
// 原始对象
const initData = {
value: 1
}
// 新的响应式对象
const data = {}
// 修改访问器属性
Object.keys(initData).forEach(key => {
Object.defineProperty(data, key, {
get() {
console.log('访问key', key)
return initData[key]
},
set(v) {
console.log('修改key', key)
initData[key] = v
}
})
})
给定一个原始对象initData,定义一个响应式对象data,默认值为空。通过遍历initData对象的键,修改data对象的访问器属性。data可以看成是对initData的拷贝,并且在每个属性上增加了get和set操作。当访问data已有属性的值时,会执行get的逻辑;当修改data已有属性的值时,会执行set的逻辑。在此我们可以通过console来查看get和set的执行时机。
控制台输入data.value,回车,可以看到"访问"信息输出。
控制台输入data.value = 3,回车,可以看到"修改"信息输出。
再次输入data.value,回车,可以看到data.value的值已变为3。并且initData的值也变为了3。
这就是vue2响应式核心原理的最简版(当然也忽略了很多细节,不过不影响说明)。
假如我们再给data增加一个属性,输入data.value1 = 1,回车,这次没有"修改"信息输出。
然后输入data.value1,回车,这次没有"访问"信息输出。
我们发现,在原有属性上访问和修改值都可以触发访问器属性操作,但属性的增加无法触发依赖收集,进而无法触发访问器属性操作。而这也是vue2中响应式存在的问题。
为此vue2中推出set用于数组或对象增删成员的响应式操作。以下是vue2中set的源码:
js
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
export function set (target: Array<any> | Object, key: any, val: any): any {
// if (process.env.NODE_ENV !== 'production' &&
// (isUndef(target) || isPrimitive(target))
// ) {
// warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
// }
// 数组操作
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 对象操作
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// if (target._isVue || (ob && ob.vmCount)) {
// process.env.NODE_ENV !== 'production' && warn(
// 'Avoid adding reactive properties to a Vue instance or its root $data ' +
// 'at runtime - declare it upfront in the data option.'
// )
// return val
// }
if (!ob) {
target[key] = val
return val
}
// 重新收集依赖
defineReactive(ob.value, key, val)
// 通知视图更新
ob.dep.notify()
return val
}
先将"process.env.NODE_ENV !== "字样的部分直接忽略,因为这部分代码不影响整体逻辑的梳理。
当目标对象为数组时,若操作的索引有效,则先将数组长度扩充为索引与原数组长度的最大值,然后利用splice方法直接修改或新增原数组对应索引值,进而再次触发了依赖的收集过程,响应式对象的值也就同步更新。
当目标对象为普通对象时,若操作的key已存在于目标对象中(仅修改操作)时,直接修改key对应的值。若操作的key不存在于目标对象中(仅新增操作)时,直接新增,并且若目标对象为响应式对象时,会重新将新{key,value}增加响应式,然后进行依赖收集,通知更新视图 。
总结一下:无论数组还是对象,用$set修改或新增属性会触发依赖收集过程,进而操作响应式对象时,会触发访问器属性。
接下来是Vue3响应式原理proxy,先看下面的代码:
js
const initData = {
value: 1
}
const proxy = new Proxy(initData, {
get(target, key, receiver) {
console.log('访问属性', target, key, receiver)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('修改属性', target, key, value, receiver)
return Reflect.set(target, key, value, receiver)
}
})
代码中通过new Proxy基于initData对象创建了代理对象proxy,其中第二个参数设置了get和set操作逻辑。
同样的操作再执行一下:
控制台输入proxy.value,回车,可以看到"访问"信息输出。
控制台输入proxy.value = 2,回车,可以看到"修改"信息输出。此时目标对象initData的value值也同步更新为2。
控制台输入initData.value1 = 1,回车,然后输入proxy.value1, 回车,可以看到"修改"信息输出。此时目标对象proxy也新增了value1属性,值也同步更新为1,同时有"访问"信息输出。
控制台输入proxy.value2 = 3,回车,然后查看initData对象中也有value2属性,值也为3。
总结一下,无论是操作目标对象,还是操作代理对象,Vue3中通过Proxy实现响应式,新增(删除)和修改属性操作都可以保持响应式同步。