一、开篇
本篇文章主要从Object
的获取层面来分析如何代理Object。在分析Object
转成响应式数据之前,建议大家尝试阅读这几篇文章:
- 保姆式地带你从0到1实现Vue3 Effect
- 完美的从0撸出来一个 Vue3 computed 实现逻辑
- 从0撸出一套Vue3 watch 的 API 结合 Proxy为什么需要Reflect,从原理层面理解它们整理出之前三篇文章的完整版的响应数据代码:(感兴趣的可以看一下,否则直接跳过)
// 存储副作用的桶
const bucket = new WeakMap();
// 原始数据
const data = { foo: "foo", bar: "bar" };
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 栈
const effectStack = [];
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key, receiver) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
// 返回属性值
return Reflect.get(target, key, receiver);
},
// 拦截设置操作
set(target, key, newVal, receiver) {
// 设置属性值
Reflect.set(target, key, newVal, receiver);
// 把副作用从桶中取出并执行
trigger(target, key);
return true;
},
});
// 在get 拦截函数内调用 track 函数 追踪变化
function track(target, key) {
// 没有 activeEffect 直接 return
if (!activeEffect) return;
// 根据 target 从 桶中取出 depsMap,它也是一个map 类型,key-->effects
let depsMap = bucket.get(target);
// 如果 depsMap 不存在,创建一个新的map,并与 target 关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 再根据 key 从depsMap 中取得 deps,它是一个 Set 集合
// 里面存储着所有与当前 key 相关的副作用函数:effects
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 最后将当前激活的副作用函数添加到桶里
deps.add(activeEffect);
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 effectFn.deps 数组中
activeEffect.deps.push(deps);
}
// 在 set 拦截函数内调用 trigger 函数 触发变化
function trigger(target, key) {
// 从桶中取出 depsMap,它也是一个map 类型,key-->effects
const depsMap = bucket.get(target);
if (!depsMap) return true;
// 再根据 key 从depsMap 中取得 副作用函数 effects
const effects = depsMap.get(key);
const effects2Run = new Set();
effects &&
effects.forEach((effectFn) => {
// 如果trigger触发执行的副作用函数与当前的副作用函数相同,则不触发执行
if (effectFn !== activeEffect) {
effects2Run.add(effectFn);
}
});
effects2Run.forEach((effectFn) => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
// 否则直接执行副作用函数
effectFn();
}
});
}
// 用于注册副作用函数
function effect(fn, options = {}) {
const effectFn = () => {
// 删除与 effectFn 相关的关系
cleanup(effectFn);
// 调用effect函数的时候,将 effectFn 赋值给 activeEffect
activeEffect = effectFn;
// 在调用副作用函数之前,将当前副作用函数添加到 effectStack 栈中
effectStack.push(effectFn);
// 执行副作用函数,得到返回值
const res = fn();
// 在调用副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
// 将返回值 return 出去
return res;
};
// 将 options 挂在到 effectFn 上
effectFn.options = options;
// effectFn.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 只有非lazy的时候才会执行
if (!options.lazy) {
effectFn();
}
// 将副作用函数作为返回值返回
return effectFn;
}
function cleanup(effectFn) {
// 遍历 effectFn.deps 数组
effectFn.deps.forEach((deps) => {
// 将 effectFn 从依赖集合中删除
deps.delete(effectFn);
});
// 清空 effectFn.deps
effectFn.deps.length = 0;
}
// 遍历 source 内所有的属性,从而触发 track 函数
function traverse(source, seen = new Set()) {
// 如果要读取的数据是原始数据,或者已经被读取过,那么什么都不做
if (typeof source !== "object" || source === null || seen.has(source)) return;
// 将数据添加到 seen 中,代表遍历地读取过了,避免循环引用引起死循环
seen.add(source);
// 暂时不考虑数组等的情况
// 假设 source 是一个对象,使用 for in 循环遍历读取每一个值,并递归调用 traverse 进行处理
for (let key in source) {
// 如果是对象,递归调用
traverse(source[key], seen);
}
return source;
}
function watch(source, cb, options = {}) {
let getter;
if (typeof source === "function") {
// 如果是function,直接作为 getter
getter = source;
} else {
// 如果是对象,监听source内所以的属性
getter = () => traverse(source);
}
// 存储用户注册过的过期回调
let cleanup;
// 定义 过期回调
function onInvalidate(fn) {
// 将过期回调存储起来
cleanup = fn;
}
const job = () => {
newVal = effectFn();
// 需要注意的是,第一次执行 watch cb的时候,不执行 过期回调,再次触发watch cb就会执行 上一个过期回调
if (cleanup) {
// 在调用cb之前,执行过期回调
cleanup();
}
cb(oldVal, newVal, onInvalidate);
oldVal = newVal;
};
// 定义旧值和新值
let oldVal, newVal;
// 使用 effect 注册将副作用函数时候,开启 lazy 选项,并把返回值存储到 effectFn ,便于后面手动调用
const effectFn = effect(() => getter(), {
lazy: true,
scheduler: () => {
if (options.flush === "post") {
// flush 为 post,放到微任务队列中执行
Promise.resolve().then(job);
} else {
job();
}
},
});
if (options.immediate) {
job();
} else {
// 手动调用,将执行的结果存储到 oldVal 中
oldVal = effectFn();
}
}
二、在Object
读取操作中添加track
1. Object
有哪些读取属性的操作
- 访问属性:
obj.key
- 判断对象或原型上是否存在给定的
key
:key in obj
- 使用
for...in
循环遍历对象:for(let key in obj){}
2. track
:obj.key
obj.key
直接通过 get
捕捉器就能 track
了
// 原始数据
const data = { foo: "foo", bar: "bar" };
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key, receiver) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
// 返回属性值
return Reflect.get(target, key, receiver);
},
});
3. track
:key in obj
看到 in
这个操作符,会一脸懵,不知道 in
到底是触发了哪个内置方法或者内置槽,那么我们就去看看 ESMAScript
是如何介绍 in
的执行逻辑的
- 让lref 的值为 RelationalExpression执行结果
- 让lval的值为 ? GetValue(
lref
) - 让 rref值为ShiftExpression执行结果
- 让rval值为 ? GetValue(
rref
). - 返回 HasProperty(
rval
, ? ToPropertyKey(lval
)).
可以看到第6步,调用了 HasProperty
这个抽象方法,那我们再看看它吧
这下就一目了然了,是调用的 [[HasProperty]]
内置方法,在 Proxy为什么需要Reflect,从原理层面理解它们 表格中已经介绍了 [[HasProperty]]
触发的是 Proxy
的 has
捕捉器。咱们就可以这么做了
代码
效果
4. track
: for(let key in obj){}
咱们就和 in
一样来分析一下吧
核心分析第6步,如果iterationKind
是一个枚举
a. 如果 exprValue
是 undefined
或者 null
,那么返回 Completion Record { [[Type]]: break, [[Value]]: empty, [[Target]]: empty }.
b. 让obj 的值为! ToObject(exprValue
)
c. 让 iterator
值是 EnumerateObjectProperties(obj
).
d. 让 nextMethod
值是 ! GetV(iterator
, "next").
e. 返回 Iterator Record { [[Iterator]]: iterator
, [[NextMethod]]: nextMethod
, [[Done]]: false }.
上面 第e步可以看出 循环获取的值 是通过iterator
迭代器获取的,那么咱么看看 iterator
是怎么从EnumerateObjectProperties得到的
EnumerateObjectProperties
是一个 generator
函数,接受的参数 obj
就是被我们for...in
循环的对象,里面有个亮眼的 代码 Reflect.ownKeys(obj)
。
原来如此 for...in
遍历的对象会触发 ownKeys
,那么我们就可以从 ownKeys
捕捉器来下手追踪了。
a. 拦截 ownKeys
,收集 effectFn
经过上面的分析,我们只需要在 ownKeys
捕捉器上添加追踪即可,但是经过查询 ownKeys
只有一个参数 target
在保姆式地带你从0到1实现Vue3 Effect分析过,建立响应关系,需要三个成员,即:target
,key
,effectFn
目前 ownKeys
只有两个 target
,effectFn
,因此我们需要重建一个唯一的id作为target
,effectFn
之间的桥梁,即: ITERATE_KEY
代码
咱么这里只做了追踪收集,却没有做trigger
触发,那咱么继续往下看
b.触发 ownKeys
收集 effectFn
思考一下 有哪些会触发 for...in
循环呢?额。。。,自问自答吧,其实是set
、delete
会触发这个操作。
其中 set
只能是新增属性,delete
只能是删除已有属性,并且删除成功了才会触发。分析了之后,咱么就继续干吧。
1) set
新增属性
代码
效果
2) delete
删除属性
delete
依赖的是 [DELETE]
内部方法,在保姆式地带你从0到1实现Vue3 Effect表中,[DELETE]
对应的捕捉器是 deleteProperty
代码
effect(() => {
console.log("EffectFn 执行了");
for (const key in obj) {
}
});
三、将响应做一层reactive
封装
// 原始数据
const data = { foo: "foo", bar: "bar" };
// 将原始数据转成响应数据
const reactive = (data) => {
return new Proxy(data, {
// 拦截读取操作
get(target, key, receiver) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
// 返回属性值
return Reflect.get(target, key, receiver);
},
// 拦截 in 操作符的捕捉器
has(target, key) {
track(target, key);
return Reflect.has(target, key);
},
ownKeys(target) {
// 对于没有key的时候,建立`target`,`effectFn`之前的桥梁
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
},
deleteProperty(target, key) {
// 检测备操作的属性是否是对象自己的属性
const hasKey = Object.prototype.hasOwnProperty.call(target, key);
// 使用 Reflect.defineProperty 删除属性
const res = Reflect.deleteProperty(target, key);
// 如果是自己的属性,且删除成功,触发去作用执行
if (hasKey && res) {
trigger(target, key, TriggerType.DELETE);
}
return res;
},
// 拦截设置操作
set(target, key, newVal, receiver) {
// 缓存旧值
const oldVal = target[key];
// 当属性存在时候,标识修改属性值,否则就是新增属性值
const type = Object.prototype.hasOwnProperty.call(target, key)
? TriggerType.SET
: TriggerType.ADD;
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver);
// 比较新旧值是否相等 并且都不是 NAN的时候才会触发副作用函数
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
// 把副作用从桶中取出并执行
trigger(target, key, type);
}
return res;
},
});
};
// 将data转成响应数据
const obj = reactive(data);
四、每次set
都是需要触发 副作用函数 执行吗?
答案肯定不是的
比如:设置相同的值,不应该触发 副作用函数
1. 设置值相同,不触发副作用函数
只需要在set
拦截器那边判断值是否相同。有个列外 NAN与自身相比是不相同的
代码
效果
2. 继承关系,不触发副作用函数
这个标题,感觉会给你产生迷糊,那我们用一个demo来演示一下问题 代码
const obj = {};
const proto = { bar: 1 };
const parent = reactive(proto);
const child = reactive(obj);
Object.setPrototypeOf(child, parent);
effect(() => {
console.log("parent.bar", parent.bar);
});
effect(() => {
console.log("child.bar", child.bar);
});
setTimeout(() => {
child.bar = 2;
}, 1000);
效果
有木有发现 我只设置了 child.bar
会触发 父级的 副作用函数
a. 分析
上面的主要是因为 [[SET]] 导致的,下面我们分析一下 这是 Proxy [[SET]]内置函数 咱么核心分析一下 6、7步,如果我们没设置set,会执行 target的[[SET]],注意了,将 Receiver(代表child) 传递进去。如果设置了,会走我们下面的代码,其实,和target的[[SET]] 一样的执行,也是将 Receiver(代表child) 传递进去
// 拦截设置操作
set(target, key, newVal, receiver) {
const res = Reflect.set(target, key, newVal, receiver);
return res;
},
咱们再看看 [[SET]]内置方法,调用 OrdinarySet(O
, P
, V
, Receiver
)
再继续看看 OrdinarySet
,调用了OrdinarySetWithOwnDescriptor(O
, P
, V
, Receiver
, ownDesc
).
再继续看看 OrdinarySetWithOwnDescriptor
,终于看到了谜底了
child
没有 bar
元素,会从 parent
那里设置,注意了 parent [[SET]] 的入参 Receiver
是从 child
那里获取的到,因此,parent 内 set捕捉器是 receiver
是 child
逻辑再捋一下:修改 child.bar
值 实际是 给 parent.bar
赋值,但是我们只想要child.bar
对应的副作用函数执行。
细心的小伙伴会看到我上面着重说的 receiver
,修改 child.bar
值 child
内的 set
的 receiver
是 child
, parent
内的 set
的 receiver
也是 child
,那么这就好做了,咱们看看代码吧
b.代码
c.效果
五、总结
obj.key
通过get
收集副作用函数key in obj
通过has
收集副作用函数for(let key in obj){}
通过ownKeys
收集副作用函数ownKeys
不提供key
,需要ITERATE_KEY
作为target
,effectFn
之间的桥梁set
设置新属性才触发副作用函数delete
只能是删除已有属性,并且删除成功了才会触发
- 设置值相同,不触发副作用函数
- 需要特殊处理
NAN
自己与自己不相等
- 需要特殊处理
- 通过代理子级修改代理父级属性,只触发子级对应的副作用函数
六、完整代码
// 存储副作用的桶
const bucket = new WeakMap();
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 栈
const effectStack = [];
// 用于没有key的时候,建立`target`,`effectFn`之前的桥梁
const ITERATE_KEY = Symbol();
// 触发 trigger 类型
const TriggerType = {
SET: "set",
ADD: "add",
DELETE: "delete",
};
// 从代理中获取被代理对象
const RAW = Symbol();
// 将原始数据转成响应数据
const reactive = (data) => {
return new Proxy(data, {
// 拦截读取操作
get(target, key, receiver) {
// 获取原始对象
if (key === RAW) {
return target;
}
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
// 返回属性值
return Reflect.get(target, key, receiver);
},
// 拦截 in 操作符的捕捉器
has(target, key) {
track(target, key);
return Reflect.has(target, key);
},
ownKeys(target) {
// 对于没有key的时候,建立`target`,`effectFn`之前的桥梁
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
},
deleteProperty(target, key) {
// 检测备操作的属性是否是对象自己的属性
const hasKey = Object.prototype.hasOwnProperty.call(target, key);
// 使用 Reflect.defineProperty 删除属性
const res = Reflect.deleteProperty(target, key);
// 如果是自己的属性,且删除成功,触发去作用执行
if (hasKey && res) {
trigger(target, key, TriggerType.DELETE);
}
return res;
},
// 拦截设置操作
set(target, key, newVal, receiver) {
// 缓存旧值
const oldVal = target[key];
// 当属性存在时候,标识修改属性值,否则就是新增属性值
const type = Object.prototype.hasOwnProperty.call(target, key)
? TriggerType.SET
: TriggerType.ADD;
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver);
// receiver 是代理对象的时候才触发副作用函数
if (target === receiver[RAW]) {
// 比较新旧值是否相等 并且都不是 NAN的时候才会触发副作用函数
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
// 把副作用从桶中取出并执行
trigger(target, key, type);
}
}
return res;
},
});
};
// 在get 拦截函数内调用 track 函数 追踪变化
function track(target, key) {
// 没有 activeEffect 直接 return
if (!activeEffect) return;
// 根据 target 从 桶中取出 depsMap,它也是一个map 类型,key-->effects
let depsMap = bucket.get(target);
// 如果 depsMap 不存在,创建一个新的map,并与 target 关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 再根据 key 从depsMap 中取得 deps,它是一个 Set 集合
// 里面存储着所有与当前 key 相关的副作用函数:effects
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 最后将当前激活的副作用函数添加到桶里
deps.add(activeEffect);
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 effectFn.deps 数组中
activeEffect.deps.push(deps);
}
// 在 set 拦截函数内调用 trigger 函数 触发变化
function trigger(target, key, type) {
// 从桶中取出 depsMap,它也是一个map 类型,key-->effects
const depsMap = bucket.get(target);
if (!depsMap) return true;
// 再根据 key 从depsMap 中取得 副作用函数 effects
const effects = depsMap.get(key);
const effects2Run = new Set();
effects &&
effects.forEach((effectFn) => {
// 如果trigger触发执行的副作用函数与当前的副作用函数相同,则不触发执行
if (effectFn !== activeEffect) {
effects2Run.add(effectFn);
}
});
// 执行新增/删除属性,才会触发 桥梁 key ITERATE_KEY 对应的 effectFn
if (type === TriggerType.ADD || type === TriggerType.DELETE) {
const interEffects = depsMap.get(ITERATE_KEY);
interEffects &&
interEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effects2Run.add(effectFn);
}
});
}
effects2Run.forEach((effectFn) => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
// 否则直接执行副作用函数
effectFn();
}
});
}
// 用于注册副作用函数
function effect(fn, options = {}) {
const effectFn = () => {
// 删除与 effectFn 相关的关系
cleanup(effectFn);
// 调用effect函数的时候,将 effectFn 赋值给 activeEffect
activeEffect = effectFn;
// 在调用副作用函数之前,将当前副作用函数添加到 effectStack 栈中
effectStack.push(effectFn);
// 执行副作用函数,得到返回值
const res = fn();
// 在调用副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
// 将返回值 return 出去
return res;
};
// 将 options 挂在到 effectFn 上
effectFn.options = options;
// effectFn.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 只有非lazy的时候才会执行
if (!options.lazy) {
effectFn();
}
// 将副作用函数作为返回值返回
return effectFn;
}
function cleanup(effectFn) {
// 遍历 effectFn.deps 数组
effectFn.deps.forEach((deps) => {
// 将 effectFn 从依赖集合中删除
deps.delete(effectFn);
});
// 清空 effectFn.deps
effectFn.deps.length = 0;
}
// 遍历 source 内所有的属性,从而触发 track 函数
function traverse(source, seen = new Set()) {
// 如果要读取的数据是原始数据,或者已经被读取过,那么什么都不做
if (typeof source !== "object" || source === null || seen.has(source)) return;
// 将数据添加到 seen 中,代表遍历地读取过了,避免循环引用引起死循环
seen.add(source);
// 暂时不考虑数组等的情况
// 假设 source 是一个对象,使用 for in 循环遍历读取每一个值,并递归调用 traverse 进行处理
for (let key in source) {
// 如果是对象,递归调用
traverse(source[key], seen);
}
return source;
}
function watch(source, cb, options = {}) {
let getter;
if (typeof source === "function") {
// 如果是function,直接作为 getter
getter = source;
} else {
// 如果是对象,监听source内所以的属性
getter = () => traverse(source);
}
// 存储用户注册过的过期回调
let cleanup;
// 定义 过期回调
function onInvalidate(fn) {
// 将过期回调存储起来
cleanup = fn;
}
const job = () => {
newVal = effectFn();
// 需要注意的是,第一次执行 watch cb的时候,不执行 过期回调,再次触发watch cb就会执行 上一个过期回调
if (cleanup) {
// 在调用cb之前,执行过期回调
cleanup();
}
cb(oldVal, newVal, onInvalidate);
oldVal = newVal;
};
// 定义旧值和新值
let oldVal, newVal;
// 使用 effect 注册将副作用函数时候,开启 lazy 选项,并把返回值存储到 effectFn ,便于后面手动调用
const effectFn = effect(() => getter(), {
lazy: true,
scheduler: () => {
if (options.flush === "post") {
// flush 为 post,放到微任务队列中执行
Promise.resolve().then(job);
} else {
job();
}
},
});
if (options.immediate) {
job();
} else {
// 手动调用,将执行的结果存储到 oldVal 中
oldVal = effectFn();
}
}
const obj = {};
const proto = { bar: 1 };
const parent = reactive(proto);
const child = reactive(obj);
Object.setPrototypeOf(child, parent);
effect(() => {
console.log("parent.bar", parent.bar);
});
effect(() => {
console.log("child.bar", child.bar);
});
setTimeout(() => {
child.bar = 2;
}, 1000);
七、参考
- ESMAScript@2023
- 《Vue.js设计与实现》