《Vue.js 设计与实现》4.7 调度执行 阅读理解

268 阅读3分钟

控制副作用函数执行次数

可调度就是,trigger 函数有能力决定副作用函数执行的时机、次数、方式。
通过调度器控制副作用函数的执行次数,思考下面例子

const data = { foo:1 }
const obj = new Proxy(data, { /* ... */ }

effect(() => {
    console.log(obj.foo)
})

obj.foo++
obj.foo++

在不调度的情况下,正常执行顺序应该是:

1
2
3

两次 obj.foo++ 导致 console.log(obj.foo) 也执行了两次 , 以结果导向来看,我们只需要最后一次 obj.foo++ 时运行其副作用函数,实现代码:

// 存储副作用函数的桶
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()

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

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

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

    // 执行副作用函数
    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
}

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

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 => {
        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
    })
}




// 注册副作用函数
effect(
    () => {
        console.log(obj.foo)
    },
    {
        scheduler (fn) {
            jobQueue.add(fn)
            flushJob()
        }
    }
)

obj.foo++
obj.foo++

console.log('结束了')

总结下执行流程

/**
 * 执行顺序分析:
 * effect(..., ...) 正常收集依赖
 * 
 * 执行 obj.foo++
 * --触发 get -> track
 * --触发 set -> trigger
 * ----执行 scheduler
 * ------向 jobQueue 中添加 fn 待执行
 * ------执行 flushJob
 * --------isFlushing 为 false,继续执行
 * --------遇到 p.then 为微任务,微任务入栈,待执行
 *
 * 执行第2个 obj.foo++
 * --触发 get -> track
 * --触发 set -> trigger
 * ----执行 scheduler
 * ------向 jobQueue 中添加 fn 待执行
 * ------执行 flushJob
 * --------isFlushing 为 true,return
 * 提取微任务栈,执行微任务
 * p.then(() => jobQueue.forEach(job => job()))
 *  .finally(() => { isFlushing = false }) 重置 isFlushing
 * 执行 console.log('结束了')
 * 
 * 一次 EventLoop 结束
 * 
 * 我认为的核心思想是:
 * 利用“微任务”的执行机制
 *
 * isFlushing 控制着副作用函数只执行一次的关键,它是在微任务执行之后才会被重置为 false,
 * isFlushing 控制着副作用函数只执行一次的关键,它是在微任务执行之后才会被重置为 false,
 * 但在 jobQueue.forEach(job => job()) 执行前,可以向 jobQueue 里 add 多个副作用函数,
 * 由于它是 set 结构,因此同一个副作用函数即使 add 多次也会被去重,
 * 因此微任务
 * p.then(() => {
 *   jobQueue.forEach(job => job())
 * })
 * 只会在同步代码结束后执行,即第二次 obj.foo++ 完毕后执行
 * 
 * 
 *(
 *  个人认为的事件循环:
 *  同步代码执行->微任务执行->宏任务执行->事件循环结束,等待下次代码执行(用户操作事件或宏任务队列执行)
 * )
 */