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 参数,实现懒执行的副作用函数。

相关推荐
恋猫de小郭1 小时前
Android Studio Cloud 正式上线,不只是 Android,随时随地改 bug
android·前端·flutter
清岚_lxn5 小时前
原生SSE实现AI智能问答+Vue3前端打字机流效果
前端·javascript·人工智能·vue·ai问答
ZoeLandia6 小时前
Element UI 设置 el-table-column 宽度 width 为百分比无效
前端·ui·element-ui
橘子味的冰淇淋~6 小时前
解决 vite.config.ts 引入scss 预处理报错
前端·vue·scss
小小小小宇8 小时前
V8 引擎垃圾回收机制详解
前端
lauo8 小时前
智体知识库:ai-docs对分布式智体编程语言Poplang和javascript的语法的比较(知识库问答)
开发语言·前端·javascript·分布式·机器人·开源
拉不动的猪8 小时前
设计模式之------单例模式
前端·javascript·面试
一袋米扛几楼989 小时前
【React框架】什么是 Vite?如何使用vite自动生成react的目录?
前端·react.js·前端框架
Alt.99 小时前
SpringMVC基础二(RestFul、接收数据、视图跳转)
java·开发语言·前端·mvc
lorogy9 小时前
【VSCode配置】运行springboot项目和vue项目
vue.js·spring boot·vscode