【拆解Vue3】reactive是如何实现的?(下篇)
本篇内容基于【拆解Vue3】reactive是如何实现的?(上篇)实现。
响应式中的for...in
在Vue中,我们也可以在副作用函数中使用for...in循环来枚举响应式对象的属性。
const data = {
foo: 1
}
const obj = reactive(data)
effect(() => {
for(const key in obj) {
console.log(key)
}
})
在JavaScript中,任何操作的底层实现都基于基本语义方法,for...in也不例外。在for...in中,使用了Reflect.ownKeys(obj)
来获取只属于对象自身的属性名,因此,我们可以在Proxy
中利用ownKeys()
,来对for...in的Reflect.ownKeys(obj)
进行拦截。
const bothNaN = (newValue, oldValue) => !(newValue === newValue || oldValue === oldValue)
const isEqual = (newValue, oldValue) => bothNaN(newValue, oldValue) || oldValue === newValue
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
if(key === 'raw') {
return target
}
return Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) {
const oldValue = target[key]
if(target === receiver.raw && !isEqual(newValue, oldValue)) {
target[key] = newValue
trigger(target, key)
}
return Reflect.set(target, key, newValue, receiver)
},
ownKeys(target) { // (1)
track(target, ITERATE_KEY) // (2)
return Reflect.ownKeys(target)
}
})
}
在(1)处,我们按照之前的思路,增加了拦截函数ownKeys(target)
。但在拦截收集副作用函数的时候犯了难,ownKeys
仅能拦截到target
对象,无法拦截具体的key
。因此,我们需要把key
补上,同时能够满足key
是唯一的,这里可以使用Symbol()
生成全局唯一的key
,供track
收集指定的for...in副作用函数。在(2)处,我们引入了Symbol
类型的值ITERATE_KEY
来唯一表示for...in副作用函数。
然而,当我们尝试运行修改后的代码,reactive
还是无法实现对for...in的响应式。仔细想想也能够理解,我们在收集的时候,key
是Symbol
类型的ITERATE_KEY
,而我们修改的是obj.foo
,此时触发的key
是foo
而不是ITERATE_KEY
。所以,我们需要修改触发部分的逻辑,当全局的ITERATE_KEY
存在时,ITERATE_KEY
对应的副作用函数也应该被触发。
function trigger(target, key) {
const propsMap = objsMap.get(target)
if(!propsMap) return
const fns = propsMap.get(key)
const iterateFns = propsMap.get(ITERATE_KEY) // (3)
const otherFns = new Set()
fns && fns.forEach(fn => {
if(fn !== activeEffect) {
otherFns.add(fn)
}
})
iterateFns && iterateFns.forEach(fn => { // (4)
if(fn !== activeEffect) {
otherFns.add(fn)
}
})
otherFns.forEach(fn => {
if(fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
}
按照我们的思路,我们在(3)处获取到ITERATE_KEY
对应的副作用函数,并在(4)处触发它。
看上去,我们似乎已经能够实现对for...in的响应式了。别急,再回味一下。我们要实现的是for...in的响应式,响应式对象的特性是什么?是响应式对象修改时触发对应的副作用函数重新执行。这里for...in遍历得到的是key
,也就是属性名,因此,只要响应式对象不再新增属性,for...in所在的副作用函数就不应该被执行。再具体点,在执行obj.bar = 2
时,新增属性触发副作用函数重新执行是正常的,而在执行obj.foo++
时,obj.foo
是已有属性,副作用函数不应该被触发。
总结一下刚刚的思路其实就两点,for...in所在的副作用函数,仅在新增属性时才能被触发,而设置属性时,不会被触发。既然如此,我们就需要在trigger
时进行区分,仅对新增操作做出响应。
const TriggerType = {
SET: 'SET',
ADD: 'ADD',
}
function trigger(target, key, type) {
const propsMap = objsMap.get(target)
if(!propsMap) return
const fns = propsMap.get(key)
const iterateFns = propsMap.get(ITERATE_KEY)
const otherFns = new Set()
fns && fns.forEach(fn => {
if(fn !== activeEffect) {
otherFns.add(fn)
}
})
if(type === TriggerType.ADD) { // (5)
iterateFns && iterateFns.forEach(fn => {
if(fn !== activeEffect) {
otherFns.add(fn)
}
})
}
otherFns.forEach(fn => {
if(fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
}
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
if(key === 'raw') {
return target
}
return Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) {
const oldValue = target[key]
const type = Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADD // (6)
if(target === receiver.raw && !isEqual(newValue, oldValue)) {
target[key] = newValue
trigger(target, key, type)
}
return Reflect.set(target, key, newValue, receiver)
},
ownKeys(target) {
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
}
})
}
按照思路,我们在(6)处判断当前的key
是否已经是target
的属性,如果是,则为SET
操作,反之则为ADD
操作。我们把type
作为参数传给trigger
,(5)处增加了一个判断,仅在ADD
操作时,才会触发for...in对应的副作用函数。
删除响应式对象中的属性
前文提到过,在JavaScript中,任何操作的底层实现都基于基本语义方法,这点同样适用于对删除操作的拦截。例如,我们可以通过delete obj.foo
删除响应式对象obj
的foo
属性,这个行为依赖JavaScript提供的内部方法[[Delete]]
,该内部方法我们可以通过Proxy
提供的deleteProperty
处理器函数进行拦截。
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const res = Reflect.deleteProperty(target, key)
if(hadKey && res) {
trigger(target, key, TriggerType.DELETE)
}
return res;
}
在reactive
中,我们新增了deleteProperty()
方法用来拦截删除操作。这里需要注意的是,我们需要检查被删除属性是否存在于响应式对象上,当属性存在时才能进行删除。同时,删除也会减少响应式对象的属性数量,因此,也应该触发for...in副作用函数。
浅响应与深响应
看到这里,我们已经完成了简单对象的响应式实现。接着,让我们定义一个复杂的响应式对象,看看能否产生预期的响应式效果。
const data = {
foo: {
bar: 1
}
}
const obj = reactive(data)
effect(() => {
console.log(obj.foo.bar)
})
可以看到,内部对象失去了响应式。回忆一下reactive
的整个实现过程,其实我们实现的响应式本质上是一种浅响应,响应式内部的对象没有进行响应式处理,它们仅仅是普通的对象。这点想明白了,后面的思路也就清晰了,我们只需要递归遍历响应式对象,给每个对象包上响应式就可以了。
const bothNaN = (newValue, oldValue) => !(newValue === newValue || oldValue === oldValue)
const isEqual = (newValue, oldValue) => bothNaN(newValue, oldValue) || oldValue === newValue
function createReactive(obj, isShallow = false) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
if(key === 'raw') {
return target
}
const res = Reflect.get(target, key, receiver)
if(isShallow) return res // (7)
if(typeof res === 'object' && res !== null) {
return reactive(res) // (8)
}
return res
},
set(target, key, newValue, receiver) {
const oldValue = target[key]
const type = Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADD
if(target === receiver.raw && !isEqual(newValue, oldValue)) {
target[key] = newValue
trigger(target, key, type)
}
return Reflect.set(target, key, newValue, receiver)
},
ownKeys(target) {
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const res = Reflect.deleteProperty(target, key)
if(hadKey && res) {
trigger(target, key, TriggerType.DELETE)
}
return res
}
})
}
function reactive(obj) { // (9)
return createReactive(obj)
}
function shallowReactive(obj) { // (10)
return createReactive(obj, true)
}
在上面这段代码实现中,在(8)处我们实现了递归遍历每一个子对象,并赋予其响应式能力。此外,考虑到我们的响应式需要分为浅响应和深响应,这里我们封装了一个工厂函数createReactive
,默认实现深响应。如需实现浅响应,在(7)处直接返回get
拦截结果就可以避免深响应。(9)(10)处,我们利用createReactive
工厂函数,可以分别创建深响应对象reactive
与浅响应对象shallowReactive
。
只读的响应式对象
只读意味着仅支持对象的读取,而不支持修改。具体到响应式对象上,只读就是仅支持get
操作而不支持set
操作,不要忘记,删除也是一种修改,我们对只读对象的deleteProperty
也要进行处理。当用户尝试对只读对象进行修改时,需要抛出对应的警告。
const bothNaN = (newValue, oldValue) => !(newValue === newValue || oldValue === oldValue)
const isEqual = (newValue, oldValue) => bothNaN(newValue, oldValue) || oldValue === newValue
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
if(key === 'raw') {
return target
}
const res = Reflect.get(target, key, receiver)
if(isShallow) return res
if(typeof res === 'object' && res !== null) {
return isReadonly ? readonly(res) : reactive(res) // (11)
}
return res
},
set(target, key, newValue, receiver) {
if(isReadonly) { // (12)
console.warn(`响应式对象的属性${key}是只读的,不能修改!`)
return true
}
const oldValue = target[key]
const type = Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADD
if(target === receiver.raw && !isEqual(newValue, oldValue)) {
target[key] = newValue
trigger(target, key, type)
}
return Reflect.set(target, key, newValue, receiver)
},
ownKeys(target) {
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
deleteProperty(target, key) {
if(isReadonly) { // (13)
console.warn(`响应式对象的属性${key}是只读的,不能删除!`)
return true
}
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const res = Reflect.deleteProperty(target, key)
if(hadKey && res) {
trigger(target, key, TriggerType.DELETE)
}
return res
}
})
}
function readonly(obj) {
return createReactive(obj, false, true)
}
在(12)(13)处,我们分别对修改和删除操作进行了处理。同时,为了与上一小节的浅响应与深响应对应,这里我们利用isReadonly
开关,递归实现了深只读。接下来让我们试着用用它。
const data = {
foo: {
bar: 1
}
}
const obj = readonly(data)
到这里,其实已经完成了对只读功能的实现。功能完成了之后,自然要考虑一下,有没有可以优化的点。有了,我们无法触发set
或deleteProperty
就无法通过trigger
来触发收集到的副作用函数。既然副作用函数不能被触发,那get
时收集副作用函数的操作就显得不必要了。
get(target, key, receiver) {
if(!isReadonly) { // (14)
track(target, key)
}
if(key === 'raw') {
return target
}
const res = Reflect.get(target, key, receiver)
if(isShallow) return res
if(typeof res === 'object' && res !== null) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
(14)处,我们仅允许非只读的响应式对象进行副作用函数的收集。
参考资料
转载自:https://juejin.cn/post/7137696240485007390