如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~
作者:前端小王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
,为浅响应和深响应做铺垫 - 解决了原型链修改属性问题