第五章 非原始值的响应式方案
5.1 理解Proxy 和 Reflect
- 我们需要了解Proxy的用法,以及知道Proxy只能代理对象,基本数据类型无法代理。
- 点击了解这两个API用法Proxy - Reflect
- 本节只要带我们了解了Proxy以及Reflect作用,主要使用Reflect来配合Proxy来实现一些功能。
- proxy就不细讲了,Reflect重点说一下。
// Reflect 主要作用
const obj = {foo: 1}
// 直接读取
console.log(obj.foo) // 1
// 使用 Reflect.get 来读取
console.log(Reflect.get(obj,'foo'), { foo : 2 }) // 输出 2而非1
- 根据上述代码,我们可以看到 Reflect传入第三个参数,相当于this。
- 使用场景: 分析。我们在某些时候,对调用proxy代理的对象中的方法,最好使用 Reflect方法,否则有时this可能不是我们预想的那样
const obj = {foo:1}
const p = new Proxy(obj,{....})
const obj = {
foo: 1,
get bar(){
return this.foo
}
}
effect(() => {
console.log(p.bar)
})
// 调用
p.foo++
-
上述代码,我们通过调用p.foo++,按理说会触发副作用函数执行。实则不然。
-
原因: 上一章实现的get,通过对 target[key] 返回属性值。其中 target 是原始对象obj,而key就是字符串'bar',所以 target[key] 相当于 obj.bar。因此,当我们使用 p.bar访问属性时,它的getter函数内的this会指向的其实是原始对象 obj,这说明我们访问的是 obj.foo。注意:所以这样在副作用函数内通过原始对象访问的它 的属性是不会建立响应式联系的。
-
解决问题: 使用 Reflect.get 函数
const p = new Proxy(obj,{
// 拦截读取操作,接受第三个参数 receiver
get(target,key,receiver){
track(target,key)
// 使用 Reflect.get 返回读取的属性值
return Reflect.get(target,key,receiver)
},
// 省略
})
- 上述代码,当我们使用代理对象 p访问 bar属性时候,那么receiver 则是p。那么问题就迎刃而解了。
5.2 JavaScript对象及Proxy的工作原理
-
对象
- 常规对象
- 必须满足11个方法,ECMA规范10.1.x
- 对于内部方法 Call,必须使用 ECMA 规范 10.2.1 给出的定义
- 对于内部方法 Construct,必须使用 ECMA 规范 10.2.2给出的定义实现
- 异质对象:非常规方法调用对象
- 常规对象
-
对象的语义实际上由对象的内部方法来决定的,书中写到包括[[GET]]在内,一个对象必须部署11个必要的方法,以及额外的两个必要方法 Call 和 Construct
-
其实代理对象和普通对象没有太大区别。区别主要在于Get的实现,如果在创建代理对象没有执行对应的拦截函数get(),那当我们访问属性的时候,则是直接访问原始对象的属性,也叫透明代理。
-
Proxy 点击查看Proxy对象内部的方法
-
Call 和 Construct 这两个内部方法只要当代理的对象时函数和构造函数时才会部署。
-
拦截删除属性的操作
const obj = {foo :1}
const p = new Proxy(obj,{
deleteProperty(target,key){
return Reflect.deleteProperty(target,key)
}
})
console.log(p.foo) // 1
delete p.foo
constole.log(p.foo) // 未定义
5.3 如何代理 Object
- 对象读取
- 访问属性 obj.foo
- 判断对象或原型上是否存在给定的 key: key in obj
- 使用for ... in 循环遍历对象: for(const key in foo) {}
const obj = { foo : 1 }
const p = new Proxy(obj, {
get(target, key ,receiver){
// 建立联系
track(target,key)
// 返回属性值
return Reflect.get(target,key,receiver)
}
})
- 通过阅读ECMA文档,找到API的具体说明,再结合下表来实现对对象的某个方法代理的确认。
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... 循环: 根据规范可以找到对应其
EnumerateObjectProperties(obj)
,这是一个抽象方法,该方法返回一个迭代器对象。 -
下面代码是满足该抽闲该方法的示例实现
function* EnumerateObjectProperties(obj){
const visited = new Set()
for(const key of Reflect.ownKeys(obj)){
if(typeof key === 'symbol') continue;
const desc = Reflect.getOwnPropertyDescriptor(obj,key)
if(desc){
visited.add(key)
if(desc.enumerable) yield key;
}
}
const proto = Reflect.getPrototypeOf(obj);
if(proto === null) return
for(const protoKey of EnumerateObjectProperties(proto)){
if(!visited.has(protoKey)) yield protoKey;
}
}
- 这是个generator函数,接受obj,obj就是for ... in遍历的对象,关键点在于使用 Reflect.ownKeys(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)
}
})
-
上述代码,拦截ownKeys操作即可间接拦截for... in循环。选择使用
ITERATE_KEY
作为追踪的key,是因为 ownKeys 函数,只能拿到目标对象,不能像get或set拿到具体操作的key。因为在读写属性值时,总能知道当前正在操作是哪个属性,所以需要在该属性与副作用函数之间建立联系即可。 -
ownKey获取属于自己的键值,不与任何具体的键绑定。这样,我们使用唯一key来标识。
-
因此我们需要在触发响应的时候也对他进行触发。
trigger(target,ITERATE_KEY)
- 使用set拦截函数来实现,对for..in的拦截
const p = new Proxy(obj,{
// 拦截设置操作
set(target,key,newVal){
// 设置属性值
const res = Reflect.set(target,key,newVal,receiver)
// 把副作用函数从桶里取出并执行
trigger(target,key)
return res
}
// 省略其他拦截函数
})
- 当p添加新属性bar,会触发set拦截函数执行。此时set拦截函数接收到的key是字符串‘bar’,因此最终调用trigger函数时也只是触发了与‘bar’相关联的副作用函数重新执行。
- 但是for...in循环是在副作用函数与
ITERATE_KEY
之间建立联系,不过和‘bar’一点儿关系没有,这是尝试执行p.bar= 2时,并不能正确的触发响应。 - 解决方法:当添加属性时,我们将哪些与
ITERATE_KEY
相关联的副作用函数也取出来执行就可以了。
function trigger(target,key){
const depsMap = buck.get(target)
if(!depsMap){
// 取得 与 key想关联的副作用函数
const effects = depsMap.get(key)
// 取得与 ITERATE_KEY 相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
// 将与 key相关联的副作用函数 添加到 effectToRun
effects && effects.forEach((effectFn) => {
if(effectFn != activeEffect){
effectsToRun.add(effectFn)
}
})
// 将与 ITERATE_KEY 想关联的副作用函数也添加到 effectsToRun
iterateEffects && iterateEffects.forEach((effectFn) => {
if(effectFn !== activeEffect){
effectToRun.add(effectRun)
}
})
effectToRun.forEach(effectFn => {
if(effectFn.ioption.scheduler){
effectFn.options.scheduler(effectFn)
}else {
effectFn()
}
})
}
}
- 当trigger函数执行,对具体操作key和
ITERATE_KEY
想关联副作用都拿出来执行。 - 问题:此时新增属性触发副作用函数没问题,不过修改已有属性则会出现问题。
const obj = {foo : 1}
const p = new Proxy(obj,{/*...*/})
effect(() => {
// for .. in 循环
for(const key in p ){
console.log(key) // foo
}
})
当我们修改 p.foo =2,这时修改属性不会对for ... in 循环产生影响。因为无论怎么修改一个属性,for... in 都只会循环一次。因此为了不需要触发副作用函数重新执行,则使用set来拦截。
const p = new Proxy(obj, {
// 拦截设置操作
set(target,key,newVal){
// 如果属性不存在,则说明是在添加新属性,否则设置已有属性
const type = Object.prototype.hasOwnProperty(target,key) ? 'SET' : 'ADD'
// 设置属性值
const res = Reflect.set(target,key,newValue,receiver)
// 把副作用函数从桶里取出并执行
trigger(target,key,type)
return res
}
// 省略其他拦截函数
})
- 上述代码,使用
Object.prototype.hasOwnProperty
检查操作的属性是否已经存在于目标对象上。根据存在与否,在设置操作类型SET
和ADD
,并且将type
传入trigger
函数,并扩展trigger
函数。 - 在
trigger
函数内,用type
来区分当前操作类型,并且只有当type
为ADD
时,才会触发ITERATE_KEY
想关联的副作用函数重新执行,这样就避免了不必要的性能损耗。 - 通常我们对操作的类型进行类型的枚举。
const TiggerType = {
SET: 'SET',
ADD: 'ADD'
}
- 对象代理,最后一步工作需要做,即删除属性操作的代理: 使用
deleteProperty
来拦截。只有满足是自己对象和并且完成删除后,在调用trigger
函数,传入新的操作类型DELETE
。因此,当操作类型为DELETE
,应该触发那些与ITERATE_KEY
相关联的副作用函数重新执行。
继续更新中
END
个人博客汇总地址:github.com/codehzy/blo…