computed 的实现原理
用法
先看看 computed 的用法
const data = reactive({
num:0,
});
const doubleData = computed(()=>data.num * 2);
document.querySelector('.data .cp').innerText = doubleData.value;
这里我们给 computed 函数传递了第一个参数,它是一个类似 getter 的回调函数,输出的是一个只读的响应式引用。为了访问新创建的计算变量的 value,我们需要像 ref 一样使用 .value property。
知道功能及用法后,接下来看看源码里是如何实现的。
源码思路剖析
computed 响应式思路
对于 computed 来说,需要实现的功能包括2部分:
computed函数传递了第一个参数,输出的是一个只读的响应式引用。- 响应式引用需要使用
.valueproperty 来访问
伪代码形式如下:
function computed(fn){
let effectFn;
// 实现响应式,响应式引用读取时才触发 fn 的执行,触发 getter
effectFn = effect(fn);
// 返回一个响应式的引用,需要使用 .value 来访问
return {
get value(){
return effectFn()
}
}
}
这段伪代码实现的难点在于只有当读取响应式引用的值时,才触发 fn 执行实现响应式。在 vue3 源码该功能的实现是通过在 effect 函数中添加 lazy 选项来实现的。
computed 实现
function computed(fn) {
const effectFn = effect(fn, { lazy: true });
const obj = {
get value() {
return effectFn();
}
};
return obj;
}
function effect(fn, options) {
const effectFn = function () {
activeEffect = fn;
// activeEffect 入栈,当存在 effect 嵌套时保存所有的 activeEffect(副作用函数)
effectStack.push(activeEffect);
// 副作用函数执行,若存在 effect 嵌套,则会重新调用 effect,将 activeEffect 入栈
const res = fn();
// 将已执行的副作用函数出栈
effectStack.pop();
// 还原副作用函数为之前的值
activeEffect = effectStack[effectStack.length - 1];
return res;
};
if (!options || !options.lazy) {
effectFn();
}
return effectFn;
}
通过添加 lazy 属性,computed 不会立即执行。当计算属性被使用时,触发 getter 执行 effect 函数收集使用过计算属性的函数。
computed 还有缓存的特性,下面看下 vue3 是怎么实现的:
function effect(fn, options) {
const effectFn = function () {
activeEffect = effectFn;
// activeEffect 入栈,当存在 effect 嵌套时保存所有的 activeEffect(副作用函数)
effectStack.push(activeEffect);
// 副作用函数执行,若存在 effect 嵌套,则会重新调用 effect,将 activeEffect 入栈
const res = fn();
// 将已执行的副作用函数出栈
effectStack.pop();
// 还原副作用函数为之前的值
activeEffect = effectStack[effectStack.length - 1];
return res;
};
// 是否为懒加载
if (!options || !options.lazy) {
effectFn();
}
// 将 options 属性挂载到 effectFn 上
effectFn.options = options;
return effectFn;
}
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap)
return;
const deps = depsMap.get(key);
//新增变量 effectToRun 保存需要执行的副作用函数,避免无线递归循环
const effectToRun = new Set();
deps && deps.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectToRun.add(effectFn);
}
});
effectToRun.forEach(effectFn => {
// 如果副函数的 options 上存在调度器 scheduler,则将 副函数传递给调度器执行
if (effectFn.options && effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
}
else {
effectFn();
}
});
}
function computed(fn) {
// 创建 dirty 变量,如果为真表示需要重新计算值
let dirty = true, value;
const effectFn = effect(fn, {
lazy: true, scheduler() {
dirty = true;
}
});
const obj = {
get value() {
if (dirty) { // 只有 dirty 为真时才计算,并将得到的值缓存到 value 中
value = effectFn();
// 将 dirty 值设置为 false, 下次访问直接使用缓存中的值
dirty = false;
}
return value;
}
};
return obj;
}
页面中首次使用计算属性时此时 dirty 为真,执行computed 中传递的函数得到计算值,同时将 dirty 设置为 false 表示直接访问缓存中的值。当计算属性依赖的值发生改变时,调度器 scheduler 执行将 dirty 值修改为 true 重新计算得到计算值。