《Vue.js 设计与实现》4.8 计算属性 computed 与 lazy 阅读理解

131 阅读3分钟

计算属性最终执行时的代码,要达到3个目的

  1. obj.foo 或 obj.bar 任何一个值被重新赋值时,自动计算 sumRes 的值
  2. sumRes 的值发生变化后 function abc () { console.log(sumRes.value) } 同时执行
  3. obj.foo 或 obj.bar 的变化同时会导致 sumRes 的变化,所以也要运行副作用函数 abc
const sumRes = computed(() => obj.foo + obj.bar)
effect(function abc () {
    console.log(sumRes.value)
})

obj.foo = 10 

我认为的关键点

  1. 计算属性 obj.value 的 getter 里面的 track 对 obj 的 value 属性相关的副作用函数进行了依赖收集。
  2. 计算属性注册副作用函数时的 scheduler 调度器方法很妙,没有把副作用函数作为参数传递并执行。这样就在sumRes 相关的 obj.foo 或 obj.bar 值改变时会直接运行相关依赖副作用函数的 scheduler。计算属性内部的 dirty = true,代表下次读取 sumRes 时不读缓存重新计算;然后 scheduler 的 trigger 调用直接触发计算属性相关的依赖函数执行,那么包裹 abc 函数的副作用函数将被执行,执行时读取 sumRes.value,从而计算属性的 value 的 getter 将被执行,最后重新计算 value,收集依赖,返回更新后的值。

附上这章最终最终的代码

// 存储副作用函数的桶
const bucket = new WeakMap()

// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 栈
const effectStack = []

const effect = (fn, options = {}) => {
    const effectFn = () => {
        // 调用 cleanup 函数完成清除工作
        cleanup(effectFn)

        // 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
        activeEffect = effectFn

        // 在调用副作用函数之前将当前副作用函数压入栈中
        effectStack.push(effectFn)
        
        // 将 fn 的执行结果存储到 res 中
        const res = fn() // 新增

        // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
        effectStack.pop() 
        activeEffect = effectStack[effectStack.length - 1]

        // console.log('执行完包装副作用函数,原始副作用函数fn=> ', fn)

        // 将 res 作为 effectFn 的执行结果返回值
        return res // 新增
    }

    // 将 options 挂在到 effectFn 上
    effectFn.options = options

    // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
    effectFn.deps = []

    // 只有非 lazy 的时候,才执行
    if (!options.lazy) { // 新增
        // console.log('注册时执行副作用函数,原始副作用函数fn=> ', fn)
        // 执行副作用函数
        effectFn()
    }

    // 将副作用函数作为返回值返回
    return effectFn // 新增
}

function cleanup (effectFn) {
    // 遍历 effectFn.deps 数组
    for (let i = 0; i < effectFn.deps.length; i++) {
        // deps 是依赖集合
        const deps = effectFn.deps[i]
        // 将 effectFn 从依赖集合中移除
        deps.delete(effectFn)
    }

    // 最后需要重置 effectFn.deps 数组
    effectFn.deps.length = 0
}

// 计算属性
function computed (getter) {
    // value 用来缓存上一次计算的值
    let value

    // dirty 标志,用来标识是否需要重新计算值,为 true 则意味着脏,需要计算
    let dirty = true

    // 把 getter 作为副作用函数,创建一个 lazy 的 effect
    const effectFn = effect(getter, {
        lazy: true, // 注册时不执行。
        scheduler () { // 执行时不直接调用副作用函数而是调用 scheduler(fn)。(对属性进行赋值操作时,进入 set 的 trigger 逻辑时执行 scheduler(fn))
            if (!dirty) {
                dirty = true
                // 当计算属性依赖的响应式数据变化时,手动调用 trigger 函数触发响应
                trigger(obj, 'value')
            }
        }
    })

    const obj = {
        // 当读取 value 时才执行 effectFn
        get value () {
            if (dirty) {
                value = effectFn()
                dirty = false
            }

            // 当读取 value 时,手动调用 track 函数进行追踪
            track(obj, 'value')

            return value
        }
    }

    return obj
}

// 原始数据
const data = { foo: 1, bar: 2 }

const obj = new Proxy(data, {
    // 拦截读取操作
    get (target, key) {
        // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
        track(target, key)

        // 返回属性值
        return target[key]
    },

    // 拦截设置操作
    set (target, key, newVal) {
        // 设置属性值
        target[key] = newVal

        // 把副作用函数从桶里取出并执行
        trigger(target, key)
    }
})

// 在 get 拦截函数内调用 track 函数追踪变化
function track (target, key) {
    // 没有 activeEffect ,直接 return
    if (!activeEffect) 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)

    // deps 就是一个与当前副作用函数存在联系的依赖集合
    // 将其添加到 activeEffect.deps 数组中
    activeEffect.deps.push(deps)
}

// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger (target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return

    const effects = depsMap.get(key)

    const effectsToRun = new Set()
    effects && effects.forEach(effectFn => {
        // 如果 trigger 触发执行的副作用函数于当前正在执行的副作用函数相同,则不触发执行
        if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn)
        }
    })
    effectsToRun.forEach(effectFn => {
        // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
        if (effectFn?.options?.scheduler) { // 新增
            effectFn.options.scheduler(effectFn) // 新增
        } else {
            effectFn()
        }
    })
}

// 定义一个任务队列
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()

// 一个标志代表是否正在刷新队列
let isFlushing = false
function flushJob () {
    // 如果队列正在刷新,则什么都不做
    if (isFlushing) return
    // 设置为 true,代表正在刷新
    isFlushing = true

    // 在微任务队列中刷新 jobQueue 队列
    p.then(() => {
        jobQueue.forEach(job => job())
    }).finally(() => {
        // 结束后重置 isFlushing
        isFlushing = false
    })
}



const sumRes = computed(() => obj.foo + obj.bar)
effect(function abc () {
    console.log(sumRes.value)
})

obj.foo = 10 

附上关键步骤的执行流程图

effect(function abc () {   console.log(sumRes.value) }).png

github 地址

gitee 地址