如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~
作者:前端小王hs
阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主
此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来
书籍:《Vue.js设计与实现》 作者:霍春阳
本篇博文将在书第5.1节至5.4节的基础上进一步总结所提到的基础概念,附加了测试的代码运行示例,方便正在学习Vue3或想分析Vue3源码的朋友快速阅读
如有帮助,不胜荣幸
前文:
 Vue3.js“非原始值”响应式实现基本原理笔记(一)
 Vue3.js“非原始值”响应式实现基本原理笔记(二)
合理触发响应
5.4节讨论了除上一节笔记中其他的合理操作
值没有发生变化
如果执行p.foo=1,那么同样不应该触发副作用函数,所以需要在set中新增一个判断,即旧值和新值是否相等,代码如下:
const p = new Proxy(obj, {set(target, key, newVal, receiver) {// 获取旧值const oldVal = target[key]const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'const res = Reflect.set(target, key, newVal, receiver)// 比较新值与旧值,只要当不全等的时候才触发响应if (oldVal !== newVal) {trigger(target, key, type)}return res},  
})
这时还需考虑一个额外情况——NaN的全等对比
- NaN === NaN // false
- NaN !== NaN // true
所以还需再加一个判断条件,代码如下:
 if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal ))
从原型上继承属性——reactive
在书中的案例中,作者定义了一个reactive函数,对Proxy进行了封装,代码如下:
function reactive(obj) {return new Proxy(obj, {// 其他逻辑})
}
这其实是第一次出现reactive,值得注意
接着书中给了一个案例代码:
const obj = {}
const proto = { bar: 1 }
const child = reactive(obj)
const parent = reactive(proto)
// 使用 parent 作为 child 的原型
Object.setPrototypeOf(child, parent)effect(() => {console.log(child.bar) // 1
})
// 修改 child.bar 的值
child.bar = 2 // 会导致副作用函数重新执行两次
分析一下各个对象的关系
- child是- obj的代理对象
- parent是- proto的代理对象
- Object.setPrototypeOf设置- parent为- child的原型
我们知道原型链,如果访问child.bar没有,那么会沿着原型链找到parent.bar,也可以说是继承
现在来分析一下child.bar会导致副作用函数执行两次的流程:
- 读取child.bar,触发child里的get
- 执行Reflect.get(obj, 'bar', receiver)
- 发现child内没有bar,读取parent.bar
- 读取parent.bar时与副作用函数建立联系
- 在第一次读取的track(obj,bar)中,child.bar也与副作用函数建立了联系
所以child.bar和parent.bar都与副作用函数建立了联系
修改child.bar时的流程:
- 调用child.set,执行Reflect.set
- child没有- bar,变为执行- parent.set
我们知道执行set就会触发trigger,所以就执行了两次副作用函数
那如何避免两次执行呢?答案是在set中进行区分两次更新
首先看一下child里的set,代码如下:
// child 的 set 拦截函数
set(target, key, value, receiver) {// target 是原始对象 obj// receiver 是代理对象 child
}
这里的receiver,一般情况就是Proxy的实例本身,在这里也就是child
可以看阮一峰ES6里的示例代码:
const handler = {set: function(obj, prop, value, receiver) {obj[prop] = receiver;return true;}
};
const proxy = new Proxy({}, handler);
const myObj = {};
Object.setPrototypeOf(myObj, proxy);myObj.foo = 'bar';
myObj.foo === myObj // true
接着看一下parent里的set,代码如下:
// parent 的 set 拦截函数
set(target, key, value, receiver) {// target 是原始对象 proto// receiver 仍然是代理对象 child
}
这里我们知道target就是parent的被代理对象proto,但为什么receiver还是child呢?
这是因为实际操作的上下文对象是child,而不是parent
那么现在可以发现一个特点,就是target是变化的,而receiver是不变的,所以可以通过判断receiver是否是target的代理对象即可
也就是obj的代理对象是child,而proto的代理对象不是child
回到问题本身的解决方案,首先是在get中添加了一个判断条件,代码如下:
function reactive(obj) {return new Proxy(obj, {get(target, key, receiver) {// 代理对象可以通过 raw 属性访问原始数据if (key === 'raw') {return target}track(target, key)return Reflect.get(target, key, receiver)}// 省略其他拦截函数})  
}
如果访问raw,那么会返回对应的值,例如child.raw,那么返回obj
接着回到set,新增一个语句,代码如下:
function reactive(obj) {return new Proxy(obj, {set(target, key, newVal, receiver) {const oldVal = target[key]const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'const res = Reflect.set(target, key, newVal, receiver)// target === receiver.raw 说明 receiver 就是 target 的代理对象if (target === receiver.raw) {if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {trigger(target, key, type)}}return res}// 省略其他拦截函数})  
}
在set中判断了target是否等于receiver.raw,如果是的话才会调用trigger
这样的话问题就解决了
总结
- 使用reactive封装Proxy,为浅响应和深响应做铺垫
- 解决了原型链修改属性问题