Vue.js 3 渐进式实现之响应式系统——第十二节:watch 的基本实现原理

往期回顾

  1. 系列开篇与响应式基本实现
  2. effect 函数注册副作用
  3. 建立副作用函数与被操作字段之间的联系
  4. 封装 track 和 trigger 函数
  5. 分支切换与 cleanup
  6. 嵌套的 effect 与 effect 栈
  7. 避免无限递归循环
  8. 调度执行
  9. 懒执行的 effect
  10. 计算属性与缓存
  11. 计算属性的 track 和 trigger

watch 的基本实现原理

上一节中我们实现了完整的计算属性功能,这一节开始我们来实现 watch。

思路

watch

所谓 watch,本质就是观测一个响应式数据,数据发生变化时通知并执行相应的回调函数。watch 的实现本质上就是利用了 effect 以及 options.scheduler 选项,下面是最简单的实现:

js 复制代码
function watch(source, cb) {
    effect(
        // 读取 source,从而建立联系
        () => source.foo,
        {
            scheduler() {
                // source 有变化时,执行cb
                cb()
            }
        }
    )
}

递归读取完整对象

然而现在的实现有一个问题,我们硬编码了对 source.foo 的读取,也就是说现在只能监听 obj.foo 的改变。实际上我们希望的是监听的是整个 source 对象的改变,也就是说当 source 对象的任意属性发生变化时,都会触发 cb 的执行。

因此我们需要封装一个通用的读取操作:

js 复制代码
function watch(source, cb) {
    effect(
        () => traverse(source),
        {
            scheduler() {
                cb()
            }
        }
    )
}

function traverse(value, seen = new Set()) {
    // 如果 value 是原始值,或者已经被读取过,则什么都不做
    if (typeof value !== 'object' || value === null || seen.has(value)) {
        return
    }

    // 将 value 添加到 seen 中代表读取过了,避免循环引用引起的死循环
    seen.add(value)

    // 暂时只考虑对象,遍历读取对象每一个属性,递归调用 traverse
    for (const key in value) {
        traverse(value[key], seen)
    }

    return value
}

如上述代码所示,在 watch 内部 的 effect 中调用 traverse 函数递归读取对象的每一个属性,使得任意属性发生变化时都能触发回调函数执行。

支持接收 getter

watch 函数除了可以观测响应式数据,还支持接收一个 getter 函数。在 getter 函数内部,用户可以指定该 watch 监听哪些响应式数据。:

js 复制代码
watch(
    () => source.foo,
    () => {
        console.log('source.foo changed')
    }
)

实现这一功能的代码如下:

js 复制代码
function watch(source, cb) {
    let getter
    if (typeof source === 'function') {
        getter = source
    } else {
        getter = () => traverse(source)
    }

    effect(
        () => getter(),
        {
            scheduler() {
                cb()
            }
        }
    )
}

获取新值和旧值

在使用 Vue.js 的 watch 时,有一个非常重要的功能,那就是能够在回调函数中得到变化前后的值:

js 复制代码
watch(
    () => source.foo,
    (newVal, oldVal) => {
        console.log('source.foo changed', newVal, oldVal)
    }
)

实现这一功能,需要充分利用 effect 函数的 lazy 选项。每次监听的数据更新时手动调用 effect 函数的返回值 effectFn 获取更新后的响应式数据的值,而上一次调用 effectFn 的结果则作为旧值:

js 复制代码
function watch(source, cb) {
    let getter
    if (typeof source === 'function') {
        getter = source
    } else {
        getter = () => traverse(source)
    }

    let oldValue, newValue

    // 使用 effect 注册副作用函数,开启 lazy 选项,把返回值储存在 effectFn 中以便之后手动调用
    const effectFn = effect(
        () => getter(),
        {
            lazy: true,
            scheduler() {
                // 在 scheduler 中重新执行副作用函数,获取新值
                newValue = effectFn()
                // 将新值和旧值作为回调函数的参数
                cb(newValue, oldValue)
                // 更新旧值
                oldValue = newValue
            }
        }
    )

    // 手动调用副作用函数,拿到的就是初始旧值
    oldValue = effectFn()
}

上述代码最下面的部分,我们手动调用 effectFn 得到的返回值就是旧值,即第一次执行得到的值。当变化发生并触发 scheduler 调度函数执行时,会重新调用 effectFn 并得到新值。这样我们就拿到了新值和旧值,进而传给回调函数。最后不要忘了用新值更新旧值:oldValue = newValue,毕竟本次更新后的新值也是下一次更新后的旧值。

已实现

目前我们实现了 watch 的基本功能,支持监听对象的任意属性变化,也支持接收 getter 函数,并且回调函数中也能获取到监听的数据的新值和旧值。

缺陷/待实现

下一节我们将继续实现关于 watch 的两个特性:

  • 立即执行的回调函数
  • 回调函数执行时机
相关推荐
star learning white6 分钟前
xm C语言12
服务器·c语言·前端
tabzzz6 分钟前
大道至简:万字漫谈前端性能监控
前端·javascript·性能优化
0思必得014 分钟前
[Web自动化] CSS基础概念和介绍
前端·css·python·自动化·html·web自动化
小胖霞16 分钟前
全栈系列(15)github Actions自动化部署前端vue
前端·node.js·github
未来魔导16 分钟前
基于 Gin 框架的 大型 Web 项目推荐架构目录结
前端·架构·gin
Irene199117 分钟前
Vue 3 项目创建方式对比(npm create vue@latest -- --typescript --eslint --prettier 自带格式化)
vue.js
foundbug99939 分钟前
Modbus协议C语言实现(易于移植版本)
java·c语言·前端
Luna-player40 分钟前
在前端中list.map的用法
前端·数据结构·list
用户479492835691544 分钟前
面试官问 React Fiber,这一篇文章就够了
前端·javascript·react.js
小徐_23331 小时前
Gemini 3做粒子交互特效很出圈?拿 TRAE SOLO 来实现一波!
前端·ai编程·trae