本文为《Vue.js设计与实现》的笔记。
1. 调度执行
可调度性是响应系统非常重要的特性。所谓可调度,指的是当trigger动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。
我们为effect设计选项参数options,允许用户指定调度器:
effect(() => {...}, {
scheduler(fn){
//...
}
})
通过sheduler,我们在trigger函数中触发副作用函数重新执行时,直接调用用户传递的调度器函数,从而把控制权交给用户。
首先修改effect函数:
function effect(fn, options = {}) {
const effectFn = () => {
...
};
// 将options挂载在effectFn上
effectFn.options = options;
effectFn.deps = [];
effectFn();
}
修改trigger函数:
function trigger(target, key) {
//...
effectToRun &&
effectToRun.forEach((effectFn) => {
// 优先执行调度器函数
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
测试:
effect(
() => {
console.log(obj.a);
},
{
scheduler(fn) {
setTimeout(fn);
},
}
);
obj.a = 123;
console.log("test");
通过在调度器函数中使用setTimeout将副作用函数放到宏任务队列中执行,实现在test打印后再执行副作用函数。结果为:
1
test
123
以上代码成功控制了副作用函数的执行顺序,下面我们来控制它的执行次数。
看一段代码:
effect(
() => {
console.log(obj.a);
}
);
obj.a++;
obj.a++;
在没有使用调度器的情况下,该段代码的结果为:
1
2
3
如果我们只关心最终结果,而不希望了解中间过程,我们希望的输出是:
1
3
为实现这一目标,我们可以考虑以下思路:
- 使用任务队列,来将副作用函数放入
- 副作用函数多次调用,但最后只执行一次,考虑去重
- 副作用函数应推迟执行,考虑异步
添加一个任务队列:
// 任务队列
const jobQueue = new Set();
// Promise实例,resolved
const p = Promise.resolve();
let isFlushing = false; // 是否正在刷新队列
function flushJob() {
if (isFlushing) return; // 正在刷新,则直接退出
isFlushing = true;
p.then(() => {
jobQueue.forEach((job) => job()); // 微任务,遍历任务队列并执行
}).finally(() => {
isFlushing = false;
});
}
定义调度器:
effect(
() => {
console.log(obj.a);
},
{
scheduler(fn) {
jobQueue.add(fn);
flushJob();
},
}
);
obj.a++;
obj.a++;
分析以上代码执行过程:
- 使用调度器时,将副作用函数添加至
jobQueue,jobQueue为Set,可自动去重 - 调用
flushJob进行刷新队列,该函数往微任务队列中添加了一个 遍历jobQueue并执行 的任务 - 通过
isFlushing作为flag,多次调用flushJob时也只会添加一个遍历的微任务
2. computed与lazy
在原先的实现中,effect接收的函数参数会立即执行。
effect(
() => {
console.log(obj.a);
}
);
以上代码会立即执行并打印。某些场景下我们不希望它理解执行,所以我们在options参数中增加一个lazy选项:
effect(
() => {
console.log(obj.a);
},
{
lazy: true,
}
);
修改effect:
function effect(fn, options = {}) {
const effectFn = () => {
// ...
fn();
// ...
};
//...
// 新增
if (!options.lazy) {
effectFn();
}
return effectFn;
}
以上代码中判断是否lazy,若为lazy则不立即执行,且effect最终会返回一个effectFn,在外部调用该返回的函数,可执行副作用函数。
单纯地手动执行副作用函数意义不大,但如果将传递给effect的函数看作一个getter,执行后有返回值,那么我们手动执行后便可取得其返回值。
effect(() => obj.a + obj.b, {
lazy: true,
});
之前的effect仅能控制执行时机,但未将执行结果返回。修改effect:
function effect(fn, options = {}) {
const effectFn = () => {
// ...
const res = fn();
// ...
return res;
};
// ...
return effectFn;
}
通过以上修改,我们已经可以实现懒执行副作用函数,且能拿到副作用函数的执行结果。
基于以上代码,我们可以实现一个基础的computed:
function computed(getter) {
const effectFn = effect(getter, { lazy: true });
const obj = {
get value() {
return effectFn();
},
};
return obj;
}
computed接收一个getter,内部将getter作为副作用函数传给effect,并设置为懒加载,内部定义一个obj,该对象的value属性是一个访问器属性,只有当读取value的值时,才会执行effectFn并将其结果作为返回值。
const data = { a: 1, b: 2 };
const obj = new Proxy(data, ...);
const sumRes = computed(() => obj.a + obj.b);
console.log(sumRes.value);
以上代码可成功运行,结果为3。只有当我们访问sumRes.value的值时,才会执行effectFn。
3. dirty
前边所实现computed仍存在一些问题,当我们多次访问sumRes.value的值时,effectFn会被多次执行,但理想状况下,obj.a和obj.b并未发生改变,不应该重新执行计算。
解决这个问题的思路,就是对值进行缓存。
修改computed:
function computed(getter) {
let value; // 用于缓存值
let dirty = true; // 标识是否需要重新进行计算,true意味着脏(需要重新计算)
const effectFn = effect(getter, { lazy: true });
const obj = {
get value() {
if (dirty) {
value = effectFn();
dirty = false; // 计算完后重置为false
}
return value;
},
};
return obj;
}
以上代码能够实现缓存,但其中的dirty只有由true改为false的过程,什么时候变为true呢?
分析一下,需要重新计算的情况,是getter所依赖的响应式数据发生了变化,也就是trigger时,此时我们需要使用上文的scheduler。
看到这里又是lazy又是scheduler可能有点迷糊了,我们再捋一捋:
- lazy:懒加载,避免
effect(fn)时fn的立即执行 - scheduler:调度器,在
effect(fn)中,fn所依赖的响应式数据发生变化时,不重新执行fn,而是执行调度器。
lazy推迟了首次执行,也就推迟了track收集依赖,而scheduler的触发需要trigger触发依赖,前提是track已完成。
修改computed:
function computed(getter) {
// ...
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true; // 在调度器中将dirty重置为true
},
});
// ...
}
4. 在effect中使用计算属性
// obj.a = 1 obj.b = 2
const sumRes = computed(() => obj.a + obj.b);
effect(() => {
console.log(sumRes.value);
});
obj.a++;
理想情况下,上面的代码应该先打印出 3 ,后打印出 4。但实际上在前边代码实现的基础上,结果为只打印出 3。
分析原因,computed所返回的对象是一个普通对象,而非proxy实例,访问value时不会触发track。
解决方法:
- 在读取计算属性的值时,手动进行track;
- 在所依赖的响应式数据发生变化时,手动进行trigger;
修改computed:
function computed(getter) {
let value; // 用于缓存值
let dirty = true; // 标识是否需要重新进行计算,true意味着脏(需要重新计算)
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true; // 在调度器中将dirty重置为true
trigger(obj, "value"); // 在所依赖的响应式数据发生变化时,手动进行trigger
},
});
const obj = {
get value() {
if (dirty) {
value = effectFn();
dirty = false; // 计算完后重置为false
}
track(obj, "value"); //在读取计算属性的值时,手动进行track
return value;
},
};
return obj;
}
此时的依赖关系如下:
computed(obj)
--- value
--- effectFn