“这是我参与更文挑战的第8天,活动详情查看: 更文挑战”
前文摘要
通过之前的文章,我们简单实现了一个响应式,并学习了 Proxy
和 Reflect
。
- Vue3 响应式原理探索Part 1 - 20行代码实现响应式
- Vue3 响应式原理探索Part 2 - 多重结构的响应式
- 一个Javascript 代理 Proxy 的成长之旅
- 了解学习 Proxy 的好朋友 - Reflect
之前的实现存在需要手动 track
和 trigger
的问题,本文将利用 Proxy
+ Reflect
实现自动的响应式。
组合 Proxy + Effect 存储
基于之前的学习,我们很容易想到,我们可以:
- 在
Proxy
的get handler
内去做响应式的track
- 在
Proxy
的set handler
内去做响应式的trigger
示例代码如下:
function reactive(target) {
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver)
// Track
return result
},
set(target, key, value, receiver) {
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if (result && oldValue != value) { // Only if the value changes
// Trigger
}
return result
}
}
return new Proxy(target, handler)
}
结合之前的代码,我们的代码具体如下:
const targetMap = new WeakMap();
function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
let dep = depsMap.get(key);
if (dep) {
dep.forEach(element => {
element();
});
}
}
function reactive(target) {
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver);
track(target, key);
return result;
},
set(target, key, value, receiver) {
let oldValue = target[key];
let result = Reflect.set(target, key, value, receiver);
if (result && oldValue != value) {
trigger(target, key);
}
return result;
}
}
return new Proxy(target, handler);
}
let param = reactive({ width: 5, height: 2 });
let size = 0;
let effect = () => {
size = param.width * param.height;
}
effect();
console.log(size); // => 10;
param.height = 3;
console.log(size); // => 15
param.width = 6;
console.log(size); // => 18
想必你已经观察到现在已经不需要手动的去调用 trigger
和 track
, 因为他们已经恰当的自动在 reactive
函数内部被调用了。
新的问题以及 activeEffect
新的问题
但我们现在碰到两个问题。假设 param 有三个属性,width
、height
、radius
, 具体如下:
...
let param = reactive({ width: 5, height: 2 });
let size = 0;
let effect = () => {
size = param.width * param.height;
}
console.log(param.radius); // 调用了 radius key 的 track
param.radius = 15; // 调用了 width、height key 的 track, 并调用了 radius key 的 trigger
console.log(param.width); // 调用了 width key 的 track
可以看到问题:
-
并不需要响应
radius
键,因为size
并不依赖它。但我们在radius
调用或修改时,都无意中触发了track
或trigger
。 -
在
effect
外部获取param.width
时,也调用了width
键的track
,实际上不需要的。
activeEffect
我们可以考虑用 activeEffect 解决这个问题。
const targetMap = new WeakMap();
let activeEffect = null // The active effect running
function track(target, key) {
if (!activeEffect) return; // 如果没有 activeEffect 就返回
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(activeEffect); // 依赖添加 activeEffect
}
...
// trigger、 reactive 两个函数实现保持不变
function effect(eff) {
activeEffect = eff // 将 eff 函数设为 activeEffect
activeEffect() // 执行 activeEffect,因为它内部会获取各个 target 的 属性,所以会收集依赖,执行相应 key 的 track。
activeEffect = null // Unset it
}
let param = reactive({ width: 5, height: 2, radius: 10 });
let size = 0;
let newWidth = 0;
effect(() => {
size = param.width * param.height;
});
effect(() => {
newWidth = param.width * 3;
});
console.log(`size is ${size}, newWidth is ${newWidth}`); // size is 10, newWidth is 15
param.width = 10;
console.log(`size is ${size}, newWidth is ${newWidth}`); // size is 20, newWidth is 30
console.log(param.width, param.radius); // 不会触发 width \ radius 的 track
param.radius = 10; // 不会触发 trigger ,因为没有被依赖到
真正关键的是
function track(target, key) {
if (!activeEffect) return; // 如果没有 activeEffect 就返回
...
dep.add(activeEffect); // 依赖添加 activeEffect
}
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}
effect(() => {
size = param.width * param.height;
});
实现非常让人惊叹。可以看到具体原理如下:
effect
将形参eff
赋值给activeEffect
,并执行,eff
因为依赖了响应式的width
height
, 会执行其相应的get handler
Proxy
内部get handler
会执行track
, 因为activeEffect
还未被重置,所以activeEffect
被添加为width
、height
的依赖activeEffect
执行后,被重置为 null,所以和eff
不相关的属性都不会被收集成依赖,进而不会触发多余的track
及trigger
小结
本文重点实现了2个技术问题:
- 利用
Proxy
+Reflect
实现了自动的响应式 - 利用
activeEffect
解决触发无效 track 及 trigger
本文参考: