持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
Reflect是什么
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers (en-US)的方法相同。Reflect不是一个函数对象,因此它是不可构造的。
Reflect与Proxy相辅相成,在我们的响应式系统实现中发挥着重要作用
receiver
不知道大家有没有注意到,前面我们在Proxy的get中是直接返回的target[key],这样其实在某些情况下是有问题的,比如:我们有一个拥有getter属性的对象,我们对这个对象进行代理
// 原始数据
const data = {
_foo: 0,
get foo() {
return this._foo
}
}
effect(() => {
console.log(dataProxy.foo)
})
dataProxy._foo++
// 0
执行代码发现只打印了一次,我们应该希望改变_foo时要再次执行相关依赖,那么为什么没有执行呢?分析一下代码,我们打印dataProxy.foo,会执行getter属性的foo函数,返回this._foo的值,那么此时的this是指向的谁呢?
因为我们在Proxy的get里面直接返回的是target[key],所以很显然,foo函数的this隐式绑定为其调用者:data对象,所以这里_foo属性并没有触发Proxy的get handler,因此并没有被我们的响应式系统所收集,我们在对_foo进行修改操作的时候也不会重新执行对应的副作用函数。
解决方法相信大家也能想到,只要我们改变this的指向为代理后的dataProxy对象不就好了吗。那么怎么实现呢,其实Proxy的get handler有第三个参数:receiver,这个参数就是代理后的对象。Reflect也有一个静态方法:get,这个方法可以帮助我们获取一个对象的属性值,并且它也有第三个参数,这个参数这里我们可以简单理解为指定对应getter、setter属性的this,所以我们只需要用Reflect.set即可完美解决:
// 对原始数据的代理
const dataProxy = new Proxy(data, {
// 拦截读取操作
get(target, key, receiver) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return Reflect.get(target, key, receiver)
},
// 拦截设置操作
set(target, key, newVal, receiver) {
// 设置属性值
// target[key] = newVa
// set同理
Reflect.set(target, key, newVal, receiver)
// 把副作用函数从桶里取出并执行
trigger(target, key)
}
})
ownKeys
当我们在副作用函数中使用for...in操作符的时候,我们也希望是响应式的。
for...in语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。Proxy提供了相应的拦截handler:ownKeys
所以我们只需要在ownKeys里面收集依赖,然后在合适的地方去执行收集的依赖即可。
收集依赖
我们首先来实现收集依赖,这一步非常简单:
const ITERATE_KEY = Symbol()
new Proxy(obj, {
// 省略部分代码
ownKeys(target) {
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
}
})
由于for...in操作没有具体的key,所以我们在track收集依赖的时候声明一个symbol值代替
执行依赖
相对于收集依赖,执行依赖就要考虑更多的情况。由于我们是对对象属性的遍历,所以我们应该只关心对象属性的增删,不考虑值的变化。
对象属性增加
对象属性的增加发生在Proxy的set handler中,所以我们在set对应值的时候应该判断该操作是修改值的操作还是增加属性的操作,这其实很简单,只需要判断target原来有没有相应的key
new Proxy(data, {
// 拦截设置操作
set(target, key, newVal, receiver) {
// 判断原对象有没有key 有type为EDIT 没有为ADD
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'EDIT' : 'ADD'
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
// 把副作用函数从桶里取出并执行 type传入trigger 在trigger中取出ITERATE_KEY对应的依赖集合执行
trigger(target, key, type)
return res
}
})
function trigger(target, key, type) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
// for...in 对应的key依赖
const iterateEffects = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
// 判断type为ADD时添加到effectsToRun集合执行
if (type === 'ADD') {
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
effectsToRun.forEach(effectFn => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
// effectsToRun.forEach(effectFn => effectFn())
}
上面代码我们在set handler中判断是否是增加属性的操作,然后将type传入trigger,trigger根据type去获取执行对应依赖
对象属性删除
在我们对对象的属性进行删除时也应该执行对应的依赖,Proxy提供了对应的handler:deleteProperty,
我们只需要在这里执行对应依赖即可:
// 对原始数据的代理
const dataProxy = new Proxy(data, {
deleteProperty(target, key) {
// 判断key是否存在
const exist = Object.prototype.hasOwnProperty.call(target, key)
// 删除操作 res是否删除成功
const res = Reflect.deleteProperty(target, key)
if (exist && res) {
// 存在且删除成功触发依赖执行
trigger(target, key, 'DELETE')
}
return res
}
})
在trigger中,我们只需要判断type时增加|| type === 'DELETE'即可
重复赋值
在set handler中,我们并没有判断新值跟旧值是否相同,在新旧值相等时,我们不应该去执行收集的依赖
// 对原始数据的代理
const dataProxy = new Proxy(data, {
// 拦截设置操作
set(target, key, newVal, receiver) {
// 获取旧值
const oldVal = target[key]
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'EDIT' : 'ADD'
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
// 判断新旧值是否相同且都不为NaN 把副作用函数从桶里取出并执行
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
// trigger(target, key, type)
return res
},
})