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操作符

更新中。。。

相关内容

热门资讯

你的晚安是我的早安是什么歌曲 你的晚安是我的早安是什么歌曲苏天伦《你的早安是我的晚安》“你的晚安是我的早安”是单小源的歌曲《东京遇...
积极进取的小故事 积极进取的小故事现代的普通人,不要名人的。不能与别人重复,尽快回答   啄木鸟的故事       啄...
熊出没之探险日记3什么时候播出... 熊出没之探险日记3什么时候播出?熊出没之探险日记3,春节前播放。熊出没只是探险日记三2020年5月4...
谁知道所有有关“七”的歌?拜托... 谁知道所有有关“七”的歌?拜托了各位 谢谢就是歌曲名里有“七”这个字的!谢谢七月七迅谈日晴 看我七十...
求一本小说 女主穿越了三次 每... 求一本小说 女主穿越了三次 每次都在福临身边 后来怀孕了孩子被打掉了那个 女主叫什么雯?那个女主就是...
如果记忆不说话,流年也会开出花... 如果记忆不说话,流年也会开出花的基本信息书 名:《如果记忆不弯饥好说话,流年也会开出花》埋铅 作 者...
你好,旧时光漫画版在哪里可以看... 你好,旧时光漫画版在哪里可以看?暂时在绘心上连载
一首英文歌,男的组合唱的,MV... 一首英文歌,男的组合唱的,MV是一个婚礼的过程。求歌名。是不是darin的can'tstoplove...
为什么很多人喜欢用胶片相机? 为什么很多人喜欢用胶片相机?有一种情怀叫做“怀旧“吧,现在数码相机越来越普遍了,已经到了”全民摄影“...
女主先爱上男主,男主却不喜欢女... 女主先爱上男主,男主却不喜欢女主或者是另有所爱,最后女主男主还是在一起的穿越小说。有木有再生缘:我的...
爱情失恋伤感句子 爱情失恋伤感句子越是美好的从前,越幸福的曾经,现在只能带来锥心的疼痛,痛到撕心裂肺,肝肠寸断,终于痛...
24岁穿这个会不会显老 24岁穿这个会不会显老有点显老,这个颜色款式,颜色有点暗,没有活力,属于那种气质佳,长得高雅的女人,...
哈尔的移动城堡英语版 哈尔的移动城堡英语版可以发给我吗度盘~请查收~
秦时明月之万里长城什么时候播 秦时明月之万里长城什么时候播据说是今年暑假开播别急,官网什么的信他你就输了,12年之前底应该会出,杭...
孩子会得抽动症吗? 孩子会得抽动症吗?我天生的气性比较大,有时跟别人斗嘴时候就会手脚哆嗦,麻木,我问一下这是不是抽动症就...
亨德尔一生为音乐献出了怎样的贡... 亨德尔一生为音乐献出了怎样的贡献?亨德尔一生写了歌剧41部,清唱剧21部,以及大量的管乐器与弦乐器的...
礼仪起源和发展的经典故事? 礼仪起源和发展的经典故事?一、礼仪的起源;1、天神生礼仪;2、礼为天地人的统一体;3、礼产生于人的自...
描写桂林山水的句子有哪些? 描写桂林山水的句子有哪些?天下风光数桂林有杨万里的“梅花五岭八桂林,青罗带绕碧玉簪”;有邹应龙的“无...
避免与强敌正面对决的成语 避免与强敌正面对决的成语避免与强敌正面对决的成语避实就虚 【近义】避重就轻、避难就易、声东击西【反义...
多愁善感类的成语 多愁善感类的成语心细如发【解释】:极言小心谨慎,考虑周密。亦作“心细于发”。【出自】:吴梅《题天香石...