执行调度
当触发trigger副作用函数重新执行时,能够决定副作用函数执行的时机、次数等。如何处理:给effect再添加一个参数,提前说明他是一个对象,因为以后还会包含其他选项。判断注册副作用函数时是否存在调度器,如果存在,则直接调用调度器函数,并把当前要注册副作用函数作为参数传递过去,由用户自己控制如何执行;否则直接执行副作用函数。
现在要改变下面执行的顺序,把123放在中间执行即23之前执行。我们可以借用调度器来完成。
js
effect(
() => {
console.log(proxyData.age);
},
);
proxyData.age++;
console.log('123')
//22
//23
//123
js
function effect(fn, options) {
const effectFn = () => {
activeEffect = effectFn;
effectStack.push(activeEffect);
fn();
activeEffect = "";
};
effectFn.deps = [];
effectFn.options = options;//新增
effectFn();
}
set(target, key, newVal) {
target[key] = newVal;
let effects = bucket?.get(target)?.get(key);
const effectsToRun = new Set(effects);
effects &&
effectsToRun.forEach((effectFn) => {
let deps = effectFn.deps;
deps.forEach((item) => {
item.delete(effectFn);
});
effectFn.deps.length = 0;
if (activeEffect !== effectFn) {
effectFn.options.scheduler ? effectFn.options.scheduler(effectFn) : effectFn();//新增
}
});
},
//接下来只需给effect函数添加第二个参数
effect(
() => {
console.log(proxyData.age);
},
{
scheduler(fn) {
setTimeout(fn)
},
}
);
接下来看下面这种情况 会打印两次age,但是这两次操作相同,我们只关心结果,忽略过程。这个功能有点类似于在 Vue.js 中连续多次修改响应式数据但只会触发一次更新,实际上 Vue.js 内部实现了一个更加完善的调度器
js
effect(() => {
console.log(proxyData.age);
});
proxyData.age++
proxyData.age++
首先我们需要一个微任务队列,还需要一个任务队列(Set),以及一个标识变量。思路是按照上面的情况会同步执行两次scheduler,在scheduler中将函数添加到任务队列,因为Set的去重效果,任务队列里只会添加一个函数,再执行微任务队列,我们还需要一个标识位用来判断微任务是否执行完,因为两次同步执行scheduler会执行两次微任务队列。
js
let jobQueue = new Set();
let p = Promise.resolve();
let flag = false;
function flushJob() {
if (flag) return;//微任务队列未完成直接退出
flag = true;
p.then(() => {
jobQueue.forEach((job) => {
job();//执行副作用,因为set去重只会有一条打印信息,即最新的信息
});
}).finally(() => {
flag = false;//修改标识代表微任务队列完成
});
}
effect(
() => {
console.log(proxyData.age);
},
{
scheduler(fn) {
jobQueue.add(fn);//添加副作用
flushJob();//刷新微任务队列
},
}
);
计算属性
计算属性是一个返回值,先看vue3计算属性使用方法
js
const fullName = computed(() => { return name.value + ' Doe'; });
要实现计算属性,首先要知道只有要使用计算属性的值时才需要触发副作用函数,即我们需要将副作用存储起来。
js
function effect(fn, options = {}) {
const effectFn = () => {
activeEffect = effectFn;
effectStack.push(activeEffect);
fn();
activeEffect = "";
return fn();//新增
};
effectFn.deps = [];
effectFn.options = options;
if (options.lazy) {//新增
return effectFn;
//return effectFn();不要直接返回一个值,这样后面不会触发副作用
} else {
effectFn();
}
}
function computed(getter) {//新增
// 把 getter 作为副作用函数,创建一个 lazy 的 effect
const effectFn = effect(getter, { lazy: true })
const obj = {
get value() {
return effectFn()
}
}
return obj
}
const sumRes = computed(() => {
console.log("changed");
return proxyData.age;
});
console.log(sumRes.value);
console.log(sumRes.value);
在上面打印两次sumRes.value,会触发两次副作用函数执行,显然这是没有必要的,所以我们需要把值缓存下来
js
function computed(getter) {
let dirty = true;
let value;
const effectFn = effect(getter, {
lazy: true,
scheduler() {//新增,当依赖改变会执行这个副作用函数
dirty = true;
},
});
const obj = {
get value() {
if (dirty) {//新增
value = effectFn();
dirty = false;
}
return value;
},
};
return obj;
}
sumRes 是一个计算属性,并且在另一个 effect 的副作用函数中读取了 sumRes.value 的值。如果此时修改 obj.foo 的值,我们期望副作用函数重新执行
js
const sumRes = computed(() => {
return proxyData.age;
});
effect(() => {
console.log(sumRes.value);
});
proxyData.age++;
proxyData.age++;
解决方法:当读取计算属性的值时,我们可以手动调用 track 函数进行追踪;当计算属性依赖的响应式数据发生变化时,我们可以手动调用 trigger 函数触发响应:
js
function computed(getter) {
let dirty = true;
let value;
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true;
trigger(obj, "value");//新增
},
});
const obj = {
get value() {
track(obj, "value");//新增
if (dirty) {
value = effectFn();
dirty = false;
}
return value;
},
};
return obj;
}
综上计算属性是由effect加上其中的lazy参数,调度函数组合实现的