Vue3源码设计与实现--非原始值的响应式方案
创始人
2024-06-03 00:39:37
0

理解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 代理对象 preturn this.foo}
}
const p =new Proxy(obj, {get(target, key) {track(target, key)//这里没有使用 Reflect.get 完成读取return target[key]// 使用 Reflect.get 返回读取到的属性值。receiver是代理对象 preturn 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 规范)

  1. 对象的实际语义是由对象的内部方法(对一个对象进行操作时在引擎内部调用的方法)指定的。

obj.foo
// 引擎内部会调用 [[Get]] 这个内部方法来读取属性值。ECMAScript 规范中使用 [[xxx]] 来代表内部方法或内部槽
  1. 对象必要的内部方法(11个)

2个额外的必要内部方法:[[Call]] [[Construct]]

区分对象和函数

  1. 如果一个对象需要作为函数调用,那么这个对象就必须部署内部方法 [[Call]] 。

  1. 所以可以通过内部方法和内部槽来区分对象。例如函数对象会部署内部方法[[Call]],而普通对象不会

  1. 内部方法具有多态性,不同类型的对象可能部署了相同的内部方法,却具有不同的逻辑。比如普通对象和 Proxy 对象都部署了 [[Get]] 这个内部方法。但是他们的内部逻辑分别是 ECMA 规范的 10.1.8 和10.5.8节定义的。

  1. 满足以下三点要求的对象就是常规对象:

  1. 对象必要的内部方法,必须使用 ECMA 规范 10.1.x节给出的定义实现;

  1. 对于内部方法 [[Call]] ,必须使用 ECMA 规范 10.2.1 节给出的定义实现;

  1. 对于内部方法[[Construct]],必须使用ECMA规范 10.2.2节给出的定义实现;

  1. 异质对象:不符合上述要求的对象,例如 Proxy 对象

  1. 创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身的内部方法和行为的,而不是用来制定被代理对象的内部方法和行为的

如何代理object

什么是读取:

  1. 访问属性:obj.foo

  1. 判断对象或原型上是否存在给定的key : key in obj

  1. 使用 for...in循环遍历对象:for (const key in obj) {}

如何拦截这些读取操作

  1. Proxy 对象部署的所有内部方法:
  1. 对于属性的读取,例如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)},
})
  1. 对于 in 操作符需要如何拦截呢?
  1. Proxy中没有与 in 操作符相关的拦截函数

  1. in 操作符的运行时逻辑:

  1. 关键在06步骤,in 操作符的运算结果是通过调用 HasProperty 的方法得到的

  1. HasProperty 方法的逻辑:

  1. 在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 //将会建立依赖关系
})
  1. 再来看看如何拦截for...in循环
  1. a中的表列出的是一个对象的所有基本语义方法,任何操作其实都是由这些基本语义方法以及他们的组合实现的,先查看for...in 规范

第6步的描述内容如下:

仔细观察6的c步骤:

让 iterator的值为?EnumerateObjectProperties(obj)。

EnumerateObjectProperties是一个抽象方法,返回一个迭代器对象,规范的14.7.5.9节给出了满足该抽象方法的示例实现:

  1. 这个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 相关联的副作用函数添加到 effectsToRuneffects && effects.forEach(effectFn => {if (effectFn !== activeEffect) {effectsToRun.add(effectFn)}})// 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRuniterateEffects && 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) returnconst 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操作符

更新中。。。

相关内容

热门资讯

12岁男孩暴雨中被司机扔半路,... 天津暴雨,一位12岁男孩乘坐网约车的经历令人揪心。司机接单后没确认“已接客”,才开两三分钟,就以“下...
永州市水稻大面积单产提升暨晚稻... (转自:祁阳新闻)7月3日,永州市水稻大面积单产提升暨晚稻生产现场推进会在祁阳召开,永州市农业农村局...
兴智教育:孩子长大后跟你亲不亲... 在亲子关系的长河中,许多家长困惑于孩子长大后为何变得疏离。其实,孩子长大后与父母是否亲近,很大程度上...
“小切口”推动“大变化” 山西... 中新网忻州7月4日电 (杨静 张红艳)“枯树枝修剪后,出门再也不用担心树枝掉下来了。”4日,山西省河...
公安部:将采取措施严打医美领域... 针对中央广播电视总台近期曝光的医美领域乱象,公安部表明将采取有力措施,严厉打击相关违法犯罪活动,切实...
安利股份:目前公司自产自用聚氨... 证券日报网讯安利股份7月4日发布公告,在公司回答调研者提问时表示,公司控股子公司合肥安利聚氨酯新材料...
明天起,这笔费用上调↑ 从7月5日(出票日期)起,民航将调整国内航线旅客运输燃油附加费征收标准,相较于此前标准分别上涨了10...
暴雨突袭车辆被淹,保险赔不赔?... 川观新闻记者 卢薇7月3日,一场突然而至的大暴雨,让成都不少市民的爱车被水淹没。截至7月4日中午12...
吉林敖东:公司将持续优化金融资... 证券日报网讯吉林敖东7月4日发布公告,在公司回答调研者提问时表示,公司投资方向围绕持有的核心优质长期...
五矿发展:提名鲁辉先生为公司董... 证券日报网讯 7月4日晚间,五矿发展发布公告称,公司董事会同意向公司股东大会提名鲁辉先生为公司董事候...