如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~
作者:前端小王hs
阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主
此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来
书籍:《Vue.js设计与实现》 作者:霍春阳
本篇博文将在书第5.1节 至5.4节 的基础上进一步总结所提到的基础概念 ,附加了测试 的代码运行示例 ,方便正在学习Vue3 或想分析Vue3源码的朋友快速阅读
如有帮助,不胜荣幸
前文: Vue3.js"非原始值"响应式实现基本原理笔记(一)
如何代理Object
"读取"是一个很宽泛的概念(原文)
在之前的笔记中,只简单的讨论了如obj.foo
这般获取对象属性值的读取,但读取有很多种,例如下面几种:
- 访问属性:obj.foo
- 判断:key in obj
- 遍历:for (const key in obj)
- ...
这一章的内容,就是针对这几种不同的读取 ,以及其他的常见行为如删除 等进行拦截
实现的逻辑主要是看操作符 对应的拦截函数
在书中是通过查阅ECMA 规范,明确操作符 运行逻辑,进而找到操作符 的运算结果是调用什么抽象方法 ,然后通过这个抽象方法 找到对应的内部方法 ,进而对比Vue3.js"非原始值"响应式实现基本原理笔记(一)提到的Proxy内部方法表,选取对应的方法拦截
拦截 in
下面仅 以in
操作符为例,我们来看一下书中是如何逐步找到拦截方法的
在ECMA-262 规范的13.10.1 (原文),找到in
操作符的运行时逻辑: 01. 让 lref 的值为 RelationalExpression 的执行结果。 02. 让 lval 的值为 ? GetValue(lref)。 03. 让 rref 的值为 ShiftExpression 的执行结果。 04. 让 rval 的值为 ? GetValue(rref)。 05. 如果 Type(rval) 不是对象,则抛出 TypeError 异常。 06. 返回 ? HasProperty(rval, ? ToPropertyKey(lval))。
关键是第6步 ,出现了HasProperty()
,然后在在ECMA-262 规范的7.3.11(原文)找到关于这个方法的逻辑:
- 断言:Type(O) 是 Object。
- 断言:IsPropertyKey(P) 是 true。
- 返回 ? O.[[HasProperty]] (P)。
可以发现这个内部方法[[HasProperty]],然后在表中找到对应的拦截函数 ------has
,如下图所示:
然后就可以在Proxy
的handler
中使用has
进行拦截了,代码如下:
js
const obj = { foo: 1 }
const p = new Proxy(obj, {
has(target, key) {
track(target, key)
return Reflect.has(target, key)
}
})
effect(() => {
'foo' in p // 将会建立依赖关系
})
在第5章 有非常多的关于ECMA 的运行时逻辑,在书中没有解释,所以笔者在这里还是简单介绍一下一些关键词(以上述in
的运行时逻辑为例):
RelationalExpression
:关系表达式,例如<
、>
、=
、in
、instanceof
、<=
、>=
等操作符,在这里指的操作符in
的左侧的表达式**ShiftExpression**
:位移操作的表达式,操作符in
的右侧的表达式lref
和lval
:RelationalExpression
会生成一个引用lref
,然后通过GetValue(lref)
得到lval
rref
和lref
:逻辑同上
拦截 for...in(遍历所有可枚举属性)
逻辑和in
是相同的,找规范找到最后发现可以使用ownKey()
去拦截
但是在ownKey
中会做一些处理,代码如下:
js
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)
}
})
注:这本书的特点就是类似于电视连续剧 ,整一章节的内容是不断累积的,所以看的时候不要间断,同时要多复习
在之前的笔记中,track
是传入target
和key
,代码如下:
js
function track(target, key) {
// 没有 activeEffect,直接 return
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
const array = Array.from(deps);
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
现在变成了传入ITERATE_KEY
,iterate
的意思是重复
为什么传入ITERATE_KEY
?
因为ownKeys
是用来获取一个对象的所有键,不是与任何具体的键 进行绑定,所以就定义一个ITERATE_KEY
作为标识,去与副作用函数进行绑定
那么当触发的时候,也同样需要传入ITERATE_KEY
,代码如下:
js
trigger(target, ITERATE_KEY)
这里需要注意的是,遍历是遍历,触发拦截是触发拦截 ,整个过程只会触发一次 ownKeys
,可以理解为执行到for..in
时就触发拦截,然后for...in
循环遍历自身可枚举的属性
添加属性对for...in的影响
在上述代码中,当执行p.bar=2
时,for...in
就会由循环一次变为两次(因为obj
变为了两个键 ),但此时不会触发与ITERATE_KEY
关联的副作用函数
原因非常简单,执行p.bar=2
,关联的是与bar
相关联的副作用函数
解决的方法也非常简单,在trigger
中把两者都添加到effectsToRun
js
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 相关联的副作用函数添加到 effectsToRun
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
// 将与 ITERATE_KEY 相关联的副作用函数也添加到effectsToRun
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
}
这里的ITERATE_KEY
是外部定义的Symbol
target
即遍历的obj
修改属性对foo...in的影响
修改属性不会对foo...in
产生影响,但需要注意的是修改属性 和新增属性 使用的都是[[Set]]
,所以需要做个区分,代码如下:
js
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;
},
// 省略其他拦截函数
});
然后再在trigger
中进行判断,如果是ADD
时才执行depsMap.get(ITERATE_KEY)
,代码如下:
js
function trigger(target, key, type) {
// 省略其他逻辑
// 只有当操作类型为 'ADD' 时,才触发与 ITERATE_KEY 相关联的副作用函数
if (type === 'ADD') {
const iterateEffects = depsMap.get(ITERATE_KEY);
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
}
在书中还提到了定义枚举类型的重要性:
js
const TriggerType = {
SET: 'SET',
ADD: 'ADD'
};
便于后期维护
删除属性对for...in的影响
在书中通过查阅规范,得知是通过deleteProperty
去拦截的,所以代码如下:
js
const p = new Proxy(obj, {
deleteProperty(target, key) {
// 检查被操作的属性是否是对象自己的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
// 使用 Reflect.deleteProperty 完成属性的删除
const res = Reflect.deleteProperty(target, key);
if (res && hadKey) {
// 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
trigger(target, key, 'DELETE');
}
return res;
}
});
这里手写是进行了一个判断,删除的属性是否属于自身,这是因为执行的逻辑可能是如delete p.a
,执行了但是这个属性不存在,所以需要进行一个判断
那么最后就是在trigger
中继续加多一个type
判断,代码如下:
js
function trigger(target, key, type) {
// 省略其他逻辑
// 只有当操作类型为 'ADD' 或 'DELETE' 时才触发
if (type === 'ADD' || type === 'DELETE') {
const iterateEffects = depsMap.get(ITERATE_KEY);
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
}
其实看到这里,能够发现在设计时是从CURD去考虑的不同情况
总结
这篇笔记主要复习了不同读取情况下的响应式实现:
- 如何拦截in操作符
- 如何拦截for...in操作符
- 当新增、修改和删除时如何实现响应式