理解Reflect
Reflect 是一个全局对象,有与 Proxy拦截器相同的方法
Reflect.get()
Reflect.set()
Reflect.apply()
...
以Reflect.get举例
const obj = { foo: 1 }
console.log(obj.foo)
// 等价于
console.log(Reflect.get(obj, 'foo'))
//但是Reflect.get可以接收第三个参数,制定receiver,可以理解为函数调用过程中的 this,
eg:
console.log(Reflect.get(obj, 'foo', { foo: 2 })) // 输出的是 2 而 不是 1
//在上一章中,实现响应式的基本代码:
const obj = {
foo: 1,
get bar() {
// target[key]时,这里的 this 指向的是原始对象 obj,最终访问的说 obj.bar,很显然在副作用函数中访问原始对象的某个属性是不会建立响应式联系的
// Reflect.get(target, key, receiver) 时,这里的 this 代理对象 p
return this.foo
}
}
const p =new Proxy(obj, {
get(target, key) {
track(target, key)
//这里没有使用 Reflect.get 完成读取
return target[key]
// 使用 Reflect.get 返回读取到的属性值。receiver是代理对象 p
return Reflect.get(target, key, receiver)
},
set(target, key, newVal) {
target[key] = newVal
//这里没有使用 Reflect.set 完成设置
trigger(target, key)
}
})
//若我们在 effect 副作用函数中通过代理对象 p 访问了 bar 属性
effect(()=>{
console.log(p.bar) //1
})
//当 effect 注册的副作用 函数执行时,会读取 p.bar 属性,它发现 p.bar 是一个访问器属 性,因此执行 getter 函数。由于在 getter 函数中通过 this.foo 读取了 foo 属性值,因此我们认为副作用函数与属性 foo 之间也会建 立联系。当我们修改 p.foo 的值时应该能够触发响应,使得副作用函 数重新执行才对。然而实际并非如此
JavaScript 对象及 Proxy 的工作原理
对象分为:常规对象和异质对象(ECMAScript 规范)
- 对象的实际语义是由对象的内部方法(对一个对象进行操作时在引擎内部调用的方法)指定的。
obj.foo
// 引擎内部会调用 [[Get]] 这个内部方法来读取属性值。ECMAScript 规范中使用 [[xxx]] 来代表内部方法或内部槽
- 对象必要的内部方法(11个)
2个额外的必要内部方法:[[Call]] [[Construct]]
区分对象和函数
- 如果一个对象需要作为函数调用,那么这个对象就必须部署内部方法 [[Call]] 。
- 所以可以通过内部方法和内部槽来区分对象。例如函数对象会部署内部方法[[Call]],而普通对象不会
- 内部方法具有多态性,不同类型的对象可能部署了相同的内部方法,却具有不同的逻辑。比如普通对象和 Proxy 对象都部署了 [[Get]] 这个内部方法。但是他们的内部逻辑分别是 ECMA 规范的 10.1.8 和10.5.8节定义的。
- 满足以下三点要求的对象就是常规对象:
-
- 对象必要的内部方法,必须使用 ECMA 规范 10.1.x节给出的定义实现;
- 对于内部方法 [[Call]] ,必须使用 ECMA 规范 10.2.1 节给出的定义实现;
- 对于内部方法[[Construct]],必须使用ECMA规范 10.2.2节给出的定义实现;
- 异质对象:不符合上述要求的对象,例如 Proxy 对象
- 创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身的内部方法和行为的,而不是用来制定被代理对象的内部方法和行为的
如何代理object
什么是读取:
-
- 访问属性:obj.foo
- 判断对象或原型上是否存在给定的key : key in obj
- 使用 for...in循环遍历对象:for (const key in obj) {}
如何拦截这些读取操作
Proxy 对象部署的所有内部方法:
对于属性的读取,例如obj.foo : 通过 get 拦截函数实现
const obj = { foo: 1 }
const p = new Proxy(obj, {
get(target, key, receiver) {
// 建立联系
track(target, key)
//返回属性值
return Reflect.get(target, key, receiver)
},
})
对于 in 操作符需要如何拦截呢?(has)
-
-
- Proxy中没有与 in 操作符相关的拦截函数
- in 操作符的运行时逻辑:
-
-
-
- 关键在06步骤,in 操作符的运算结果是通过调用 HasProperty 的方法得到的
- HasProperty 方法的逻辑:
-
-
-
- 在03中可以看出,HasProperty抽象方法的返回值是通过调用对象的内部方法[[HasProperty]]得到的,在a 中查表可得出:它对应的拦截函数是has 因此:
-
const obj = { foo: 1 }
const p = new Proxy(obj, {
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
})
//这样在副作用函数中通过 in 操作符操作响应式数据时,就能够简历依赖关系:
effect(()=> {
'foo' in p //将会建立依赖关系
})
再来看看如何拦截for...in循环(ownKeys)
-
-
- a中的表列出的是一个对象的所有基本语义方法,任何操作其实都是由这些基本语义方法以及他们的组合实现的,先查看for...in 规范
-
第6步的描述内容如下:
仔细观察6的c步骤:
让 iterator的值为?EnumerateObjectProperties(obj)。
EnumerateObjectProperties是一个抽象方法,返回一个迭代器对象,规范的14.7.5.9节给出了满足该抽象方法的示例实现:
-
-
- 这个generator函数接收一个参数obj(for...in)循环遍历的对象,关键点是Reflect.ownKets(obj)来获取属于对象自身拥有的键值,但是无法获取键值绑定的键名。可以使用 ownKeys 拦截函数来拦截 Reflect.ownKeys 操作:
-
const obj ={ foo: 1 }
const ITERATE_KEY = Symbol()
const p = new Proxy(obj, {
ownKeys(target) {
//将副作用函数与 ITERATE_KEY 关联
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
}
})
// 将ITERATE_KEY作为追踪的 key 是因为在 set/get 中,可以获取到具体操作的key ,而ownKeys只是获取到
//目标target,所以使用唯一的key symbol来绑定
//触发:
trigger(target, ITERATE_KEY)
示例情况
const obj ={ foo: 1 }
// const ITERATE_KEY = Symbol()
const p = new Proxy(obj, {/*...*/})
effect(()=>{
for(const key in p) {
console.log(key) //foo
}
})
p.bar = 2
// 对 p 添加熟悉 bar for...in 会由循环一次变为执行两次。对象添加新属性时,会对for...in循环产生影响
// 但是目前对 p 的修改还不会触发副作用函数的重新执行
//原因:原有的 set 拦截函数的实现:
const p = new Proxy(obj, {
// 拦截设置操作
set(target, key, newVal, receiver) {
const res = Reflect.set(target, key, newVal, receiver)
trigger(target, key)
return res
},
//省略其他拦截函数
})
//原因:此时 set 拦截函数接收到的 key 就是字符串‘bar’ ,因此trigger 函数也只是触发了与 ‘bar’相关联的
//副作用函数重新执行,跟‘ITERATE_KEY’没有关系
//因此,我们需要将那些与 ITERATE_KEY 相关联的副作用函数也取出来执行:
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
// 取得与key 相关联的副作用函数
const effects = depsMap.get(key)
// 取得与 ITERATE_KEY 相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
// 将与 key 相关联的副作用函数添加到 effectsToRun
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
// 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
}
// 当我们修改p.foo 的值时:
p.foo = 2
// 修改属性不会对for ... in 循环产生影响,仍然只会执行一次,不需要触发副作用函数的重新执行
// 修改与增加属性值的基本语义都是 [[ set ]],所以为了提升性能,我们需要在set拦截函数中判断操作的类型
// 如果是set,那说明只是修改,原有的[[set]]就可以捕获到,
// 如果是add,那说明原有对象中没有这个键名,而且会刷新for...in循环,ownKeys拦截器中track函数绑定
// 了新增键值的symbol键名,但是此时[[set]]中的trigger函数没能绑定到这个symbol键名
// 所以修改后的set和trigger函数为:
const p = new Proxy(obj, {
// 拦截设置操作
set(target, key, newVal, receiver) {
//如果属性值不存在,则说明书在添加新属性,否则是设置已有属性
const type = Object.prototype.hasOwnProperty.call(target,key) ? 'SET' : 'ADD'
const res = Reflect.set(target, key, newVal, receiver)
// 将 type 作为第三个参数传递给 trigger 函数
trigger(target, key, type)
return res
},
//省略其他拦截函数
})
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const iterateEffects = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
// 增加判断是否为 ADD 类型,是才触发symbol的ITERATE_KEY相关联的副作用函数重新执行
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()
}
})
}
e. 代理delete操作符()
delete 操作符的行为依赖 [[ Delete ]] 内部方法,该内部方法使用 deleteProperty 拦截
const p = new Proxy(obj, P{
deleteProperty(target, key) {
// 检查被操作的属性是否是对象自己的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 使用 Reflect.deleteProperty 完成属性的删除
const rees = Reflect.deleteProperty(target, key)
if(res && hadKey) {
//只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
trigger(target, key, 'DELETE')
}
return res
}
})