vue3 源码学习 --- computed

131 阅读3分钟

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部分:

  1. computed 函数传递了第一个参数,输出的是一个只读响应式引用
  2. 响应式引用需要使用 .value property 来访问

伪代码形式如下:

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 重新计算得到计算值。