响应式中的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] = newValuetrigger(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) returnconst 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) returnconst 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] = newValuetrigger(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.ADDif(target === receiver.raw && !isEqual(newValue, oldValue)) {target[key] = newValuetrigger(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 resif(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.ADDif(target === receiver.raw && !isEqual(newValue, oldValue)) {target[key] = newValuetrigger(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 resif(typeof res === 'object' && res !== null) {return isReadonly ? readonly(res) : reactive(res)}return res
}
(14)处,我们仅允许非只读的响应式对象进行副作用函数的收集。
最后,为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。
有需要的小伙伴,可以点击下方卡片领取,无偿分享