0%

响应式数据与副作用函数收集

一个响应式系统:

  • 当读取操作发生时,将副作用函数收集到桶里
  • 当设置操作发生时,从桶里取出副作用函数执行

为了让副作用函数无论是什么形式都能被收集到桶里,设置一个affectEffect全局变量来存储被注册的副作用函数

1
2
3
4
5
6
7
8
//用一个全局变量存储被注册的副作用函数
let activeEffect;
//副作用函数栈,解决effect函数嵌套时,内层副作用覆盖activeEffect的值
let effectStack = [];
function effect(fn, options = {}) {
activeEffect = fn;
fn()
}

分支切换时清除遗留副作用函数

1
2
3
4
const data={ok:true,text:'hello'}
effect(function effectFn(){
document.body.innerText=obj.ok?obj.text:'not'
})

当obj.ok改为false时,此时obj.text不会被读取,只会触发obj.ok的读取操作,理想情况下副作用函数effectFn不应该被字段obj.text所对应的依赖集合收集,然而,整个依赖关系仍然保持,则单修改obj.text会重新执行副作用函数,这是不应该的,解决这个问题,需要在每次副作用函数执行时,把它从所有与之关联的依赖集合中删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let activeEffect
function effect(fn){
const effectFn=()=>{
//当effectFn执行,将其设置为当前激活的副作用函数
activeEffect=effectFn
fn()
}
//activeEffect.dep用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps=[]
//执行副作用函数
}
const bucket = new WeakMap();
//在get拦截函数内调用track函数追踪变化
function track(target, key) {
//禁止追踪时直接返回
if (!activeEffect || !shouldTrack) 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()));
}
//把当前激活的副作用函数添加到依赖集合deps中
deps.add(activeEffect);
//将其添加到activeEffect.deps数组中
activeEffect.deps.push(deps);
}
//避免副作用函数产生遗留
function cleanup(effectfn) {
//遍历副作用函数的依赖集合数组
for (let i = 0; i < effectfn.deps.length; i++) {
//deps是依赖集合
let deps = effectfn.deps[i];
//将该副作用函数从相关的依赖集合中移除
deps.delete(effectfn);
}
effect.deps.length = 0;
}

解决无限循环

但是这样会引起无限循环,在trigger函数中,我们遍历effects集合,执行副作用函数,当副作用函数执行,cleanup清除,就是从effects集合中将当前执行的副作用函数删除,但是副作用函数的执行又会导致其重新被收集到集合中华,造成无限循环,可以构造另外一个Set集合并遍历它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectsToRun = new Set(effects);

effects &&
effects.forEach((effectfn) => {
if (activeEffect != effectfn) {
//只有当trigger触发执行的副作用函数和当前正在执行的副作用函数不相同时才触发执行,如果副作用函数中执行obj.foo++,则会读取obj.foo的值又会设置obj.foo的值,track函数操作将副作用收集到桶中,trigger函数将副作用函数拿出来来执行,上一个副作用函数还没执行完毕就要执行下一次,否会出现栈溢出,
effectsToRun.add(effectfn);
}
});
effectsToRun.forEach((effectfn) => effectfn());
//effect&&effect.forEach(fn=>fn())//会产生无限执行
}

嵌套effect与effect栈:
当在组件Foo中渲染另一个组件,会发生effect嵌套:

1
2
3
4
5
6
7
8
const Bar={
render(){}
}
const Foo={
render(){
return <Bar/>
}
}

此时就发生了effect嵌套:

1
2
3
4
5
6
7
effect(()=>{
Foo.render()
//嵌套
effect(()=>{
Bar.render()
})
})

然而,如果只是用activeEffect来存储通过effect函数注册的副作用函数,这意味着任意时刻activeEffect所存储的副作用函数只有一个,当副作用函数发生嵌套时,内层副作用函数的执行会覆盖activeEffect的值,并且永远不会恢复到原来的值,为了解决这个问题,我们需要一个副作用函数栈effectStack,在副作用函数执行时,将当前副作用函数入栈,待副作用函数执行完毕将其从栈中弹出,并始终让activeEffect指向栈顶的副作用函数,这样就能做到一个响应式数据只会收集直接读取其值的副作用函数,而不会出现互相影响的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//用一个全局变量存储被注册的副作用函数
let activeEffect;
//副作用函数栈,解决effect函数嵌套时,内层副作用覆盖activeEffect的值
let effectStack = [];
//options选项可以实现调度执行,懒执行等
function effect(fn, options = {}) {
const effectfn = () => {
//调用cleanup完成清除工作
cleanup(effectfn);
//副作用函数入栈
effectStack.push(effectfn);
//当effectfn执行时,将其设置为当前激活的副作用函数
activeEffect = effectfn;
//副作用函数执行完毕,出栈
const res = fn();
effectStack.pop();
//把activeEffect还原为之前的值
activeEffect = effectStack[effectStack.length - 1];
return res;
};
//activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectfn.deps = [];
if (!options.lazy) {
effectfn();
}
return effectfn;
}