Vue.js 3 渐进式实现之响应式系统——第八节:调度执行

往期回顾

  1. 系列开篇与响应式基本实现
  2. effect 函数注册副作用
  3. 建立副作用函数与被操作字段之间的联系
  4. 封装 track 和 trigger 函数
  5. 分支切换与 cleanup
  6. 嵌套的 effect 与 effect 栈
  7. 避免无限递归循环

基础调度执行

上一节中我们解决了无限递归循环的问题。

本节将为我们的响应式系统实现调度执行。这一功能十分重要,是后续实现 computed 和 watch 的基础。

思路

可调度性

可调度性是响应式系统非常重要的特性。

所谓可调度,指的是当响应式数据变化触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。

我们可以为 effect 函数设计一个选项参数 options,允许用户指定调度器;当 trigger 动作触发副作用函数执行时,把副作用函数交给调取器执行,从而把控制权交给用户。

js 复制代码
effect(
    // 副作用函数
    () => {
        /* ... */
    }, 
    // options 参数
    {
        // 调度器 scheduler 是一个函数
        scheduler(effectFn) {
            /* ... */
        }
    }
)

代码

js 复制代码
// 用一个全局变量存储被注册的副作用函数
let activeEffect

// 新增 effect 栈
const effectStack = []

// effect 函数用于注册副作用函数
// 新增参数 options,用来指定调度器(options 对象的 scheduler 属性)
function effect(fn, options = {}) {
    // 包装真实副作用函数的函数,包含清除和再收集逻辑
    const effectFn = () => {
        // 调用 cleanup 完成清除工作
        cleanup(effectFn)
        
        // effectFn 每次执行都会重新进行一次依赖收集,并且被收集的副作用函数就是它自己
        activeEffect = effectFn

        // 调用真实副作用函数之前,先将当前 effect 压入栈中
        effectStack.push(effectFn)

        // 在 fn 这次执行中,effectFn 会作为副作用函数被收集到 fn 读取了的依赖的集合中
        fn()

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

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

    // effctFn.deps 数组用来储存该副作用函数的所有依赖集合
    effectFn.deps = []
    // 执行副作用函数
    effectFn()
}

function cleanup(effectFn) {
    // 遍历 deps 数组
    effectFn.deps.forEach( deps => {
        // 把副作用函数从集合中删除
        deps.delete(effectFn)
    })

    // 重置副作用函数的依赖集合,因为之后要再重新收集一次的
    effectFn.deps.length = 0
}

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

// 原始数据
const data = { text1: 'text1', text2: 'text2' }
// 对原始数据的代理
const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
        // 把副作用函数收集到桶中
        track(target, key)
        // 返回属性值
        return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
        // 设置属性值
        target[key] = newVal
        // 把副作用函数从桶里取出并执行
        trigger(target, key)
        // 返回 true 代表设置操作成功
        return true
    }
})

// track 函数 在 get 拦截函数中被调用,用来追踪副作用函数
function track(target, key) {
    // 没有 activeEffect 直接 return
    if (!activeEffect) return target[key]

    // 根据 target 从"桶"中取得 depsMap,也是Map类型:key --> effects
    let depsMap = bucket.get(target)
    // 如果不存在 depsMap,就新建一个 Map 并与 target 关联
    if (!depsMap) {
        depsMap = new Map()
        bucket.set(target, depsMap)
    }

    // 再根据 key 从 depsMap 中取得 deps。
    // deps是一个 Set 类型,储存所有与当前 key 相关联的副作用函数
    let deps = depsMap.get(key)
    // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
    if (!deps) {
        deps = new Set()
        depsMap.set(key, deps)
    }

    // 最后将当前激活的副作用函数添加到"桶"里
    deps.add(activeEffect)

    // 也将这个集合添加到副作用函数的 deps 数组中
    activeEffect.deps.push(deps)
}

// trigger 函数 在 set 拦截函数中被调用,用来触发更新
function trigger(target, key) {
    const depsMap = bucket.get(target)
    // 如果这个对象没有被追踪的依赖,没有需要重新运行的副作用函数,直接 return
    if (!depsMap) return

    const effects = depsMap.get(key)

    // 新建空集合存储本次触发更新要执行的副作用函数
    const effectsToRun = new Set()

    // 如果当前 activeEffect 在依赖集合里,本次触发更新不执行它
    effects?.forEach(effectFn => {
        if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn)
        }
    })

    effectsToRun.forEach(effectFn => {
        // 如果一个副作用函数存在到调度器,则调用该调度器,并将副作用函数作为参数传入
        if (effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn)
        } else {
            effectFn()
        }
    })
}

已实现

我们给 effect 函数新增了一个参数 options,用 options 的 scheduler 属性来允许用户指定执行副作用函数的调度器,实现了响应式系统的可调度性。

缺陷/待实现

下一节中,我们将同样利用 options 参数,实现懒执行的副作用函数。

相关推荐
RaidenLiu10 分钟前
告别陷阱:精通Flutter Signals的生命周期、高级API与调试之道
前端·flutter·前端框架
非凡ghost10 分钟前
HWiNFO(专业系统信息检测工具)
前端·javascript·后端
非凡ghost12 分钟前
FireAlpaca(免费数字绘图软件)
前端·javascript·后端
非凡ghost18 分钟前
Sucrose Wallpaper Engine(动态壁纸管理工具)
前端·javascript·后端
拉不动的猪20 分钟前
为什么不建议项目里用延时器作为规定时间内的业务操作
前端·javascript·vue.js
该用户已不存在27 分钟前
Gemini CLI 扩展,把Nano Banana 搬到终端
前端·后端·ai编程
地方地方29 分钟前
前端踩坑记:解决图片与 Div 换行间隙的隐藏元凶
前端·javascript
jason_yang33 分钟前
vue3+element-plus按需自动导入-正确姿势
vue.js·vite·element
小猫由里香34 分钟前
小程序打开文件(文件流、地址链接)封装
前端
Tzarevich37 分钟前
使用n8n工作流自动化生成每日科技新闻速览:告别信息过载,拥抱智能阅读
前端