《Vuejs设计与实现》读书笔记
文章目录
第二篇 响应系统
第四章 响应系统的作用与实现
设计完善的响应式系统
借助 Proxy
,我们实现一个基本的响应式系统。书中代码我改成可以在 Node
中运行的代码了。
const bucket = new Set();
const data = { text: 'hello world' };
const obj = new Proxy(data, {
get(target, key) {
.add(effect);
bucketreturn target[key];
,
}set(target, key, newVal) {
= newVal;
target[key] .forEach(fn => fn());
bucketreturn true;
};
})
function effect() {
console.log(obj.text);
}
effect();
setTimeout(() => {
.text = 'Hello Vue';
obj, 1000);
}// Output:
// hello world
// Hello Vue
这里是写死了 effect
,不过不用太过纠结。
基本思路是:
- 读取的时候,副作用函数执行的时候,触发读操作,并把副作用函数放入桶内。
- 修改时,拦截写操作,执行副作用函数。
由于我们的副作用函数是硬编码,使得我们的代码非常不好维护。为此,我们还需要一个依赖收集函数来收集副作用函数。
const bucket = new Set();
const data = { text: 'hello world' };
let activeEffect;
function effect(fn) {
= fn;
activeEffect fn();
}
const obj = new Proxy(data, {
get(target, key) {
if (activeEffect) {
.add(activeEffect);
bucket
}return target[key];
,
}set(target, key, newVal) {
= newVal;
target[key] .forEach(fn => fn());
bucketreturn true;
};
})
effect(() => {
console.log(obj.text);
;
})
setTimeout(() => {
.text = 'Hello Vue';
obj, 1000); }
但如果我们为 obj
添加一个不存在的属性,我们会发现副作用方法仍然被执行了。而且,如果我们添加多个
target ,只要任意一个 target
被执行,就会引起所有的副作用函数被执行。因为我们并没有在副作用函数与数据字段之间建立起明确的关系,为此我们需要重新设计我们的数据结构。
const bucket = new WeakMap();
const data = { text: "hello world" };
let activeEffect;
function effect(fn) {
= fn;
activeEffect fn();
}
const obj = new Proxy(data, {
get(target, key) {
if (!activeEffect) return target[key];
let depsMap = bucket.get(target);
if (!depsMap) {
.set(target, ( depsMap = new Map() ));
bucket
}let deps = depsMap.get(key);
if (!deps) {
.set(key, (deps = new Set()));
depsMap
}.add(activeEffect);
depsreturn target[key];
,
}set(target, key, value) {
= value;
target[key] const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
&& effects.forEach(fn => fn());
effects
};
})
effect(() => {
console.log(obj.text);
;
})
setTimeout(() => {
.text = "Hello Vue";
obj, 1000); }
首先,我们需要用 WeakMap
来收集 target
,确保对应的 target
变动只会影响与之有关的副作用函数。使用
WeakMap
可以确保引用关系是弱引用,从而不影响 GC 。
其次,我们收集 target
对应的 key
的副作用函数,这些函数将构成一个集合,在需要的时候被执行。
我们简单重构一下代码:
const bucket = new WeakMap();
const data = { text: "hello world" };
let activeEffect;
function effect(fn) {
= fn;
activeEffect fn();
}
const obj = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key];
,
}set(target, key, value) {
= value;
target[key] trigger(target, key);
};
})
function track(target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
.set(target, ( depsMap = new Map() ));
bucket
}let deps = depsMap.get(key);
if (!deps) {
.set(key, (deps = new Set()));
depsMap
}.add(activeEffect);
deps
}
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
&& effects.forEach(fn => fn());
effects
}
effect(() => {
console.log(obj.text);
;
})
setTimeout(() => {
.text = "Hello Vue";
obj, 1000); }
思考以下代码:
const data = { ok: true, text: "hello world" };
// 前略
effect(() => {
console.log(obj.ok ? obj.text : "not");
; })
当 obj.ok
为 true
时,依赖收集会为我们收集
ok
和 text
,为 false
时,会收集到 ok
字段。但 obj.ok
是会变动的,而这种变动显然会引起依赖的变动,而目前我们的代码显然无法收集这种变动。这会引起依赖变动的缺失和冗余。为了解决这个问题,我们可以副作用函数执行时,重新绑定依赖。
const bucket = new WeakMap();
const data = { text: "hello world" };
let activeEffect;
function effect(fn) {
const effectFn = () => {
// 切断依赖
cleanup(effectFn);
= effectFn;
activeEffect // 重新收集依赖
fn();
;
}.deps = [];
effectFneffectFn();
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
.delete(effectFn);
deps
}.deps = [];
effectFn
}
const obj = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key];
,
}set(target, key, value) {
= value;
target[key] trigger(target, key);
};
})
function track(target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
.set(target, ( depsMap = new Map() ));
bucket
}let deps = depsMap.get(key);
if (!deps) {
.set(key, (deps = new Set()));
depsMap
}.add(activeEffect);
deps// 添加依赖
.deps.push(deps);
activeEffect
}
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
// 复制一份 Set ,防止原依赖集合变动从而引起循环
const effectsToRun = new Set(effects);
.forEach(fn => fn());
effectsToRun
}
effect(() => {
console.log(obj.text);
;
})
setTimeout(() => {
.text = "Hello Vue";
obj, 1000); }
嵌套 effect
我们当前的实现并不支持嵌套 effect ,因为我们使用了全局变量
ativeEffect
来注册 effect
函数,在 effect
嵌套时,就会引起覆盖。
const data = { foo: true, bar: true };
let temp1, temp2;
// 省略
effect(() => {
console.log('effectFn1 执行');
effect(function effectFn2() {
console.log('effectFn2 执行');
= obj.bar;
temp2 ;
})= obj.foo;
temp1 ;
})
setTimeout(() => {
.foo = "Hello Vue";
obj, 1000);
}
// Output:
// effectFn1 执行
// effectFn2 执行
// effectFn2 执行
我们需要一个 effectStack
来解决这个问题。
let activeEffect;
let effectStack = [];
function effect(fn) {
const effectFn = () => {
cleanup(effectFn);
.push(effectFn);
effectStack= effectFn;
activeEffect fn();
.pop();
effectStack= effectStack[effectStack.length - 1];
activeEffect ;
}.deps = [];
effectFneffectFn();
}
activeEffect
相当于栈顶指针, effectStack
相当于执行栈。当嵌套发生时,位于栈顶的 effect
则始终等于当前值的 effect
,从而避免发生错乱。
避免无限循环
如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。
const data = { foo: 1 };
// 省略
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectsToRun = new Set();
// 过滤与当前执行 effect 相同的副作用函数
&& effects.forEach(effectFn => {
effects if (effectFn !== activeEffect) {
.add(effectFn);
effectsToRun
};
}).forEach(effectFn => effectFn());
effectsToRun
}
effect(() => {
.foo++;
obj; })
未完待续。
文章作者 bigshans
上次更新 2023-01-08