《Vue.js设计与实现》——watch的实现原理

《Vue.js设计与实现》------响应式系统的基本实现

《Vue.js设计与实现》------分支切换与 cleanup

《Vue.js设计与实现》------副作用函数实现可嵌套

《Vue.js设计与实现》------响应式系统的可调度性

你来说说computed的实现原理吧,lazy与计算属性computed------《Vue.js设计与实现》

上一篇文章中我们实现了一个computed ,这一篇文章中我们来实现一个在Vue同样重要且常用的api---------watch(这两个一起出现就很容易想到:说说computed和watch的区别吧🙏),文章中需要的但是没有列出来的方法在前几篇文章中,如果能串下来看会没那么懵逼(绝对不是为了加阅读量的引流说法🔊)

watch的基本实现

常见的一种watch的调用方式如下:

javascript 复制代码
const obj = { foo: 1 }
const proxyObj = new Proxy({
    // ...省略
})

watch(proxyObj, () => {
    console.log('proxyObj 里的数据变动了');
})

经过前面的学习,可以很容易想到,这不就是响应式数据proxyObj的数据改动后,在调度器scheduler调用给定的回调函数就可以了吗。唉,你他*的还真是个天才(李云龙音),撸起袖子就是干,一个基本的watch函数如下:

javascript 复制代码
function watch(proxyObj, cb) {
    effect(() => proxyObj.foo, {
        scheduler() {
            cb()
        }
    })
}

watch函数中调用了effect函数并传递了调度器使得obj.foo更改时能够走到调度器中,然后就能执行我们传递过去的想要执行的cb了。试试更改proxyObj.foo是否可以触发回调。

javascript 复制代码
proxyObj.foo++

watch监听所有属性

上一小节的实现中,我们硬编码了对proxyObj.foo的watch,现在我们拓展一下,使得proxyObj上的任一属性更改了都能触发回调,添加traverse函数遍历响应式对象。

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

// traverse函数来源于《Vue.js设计与实现》,感恩💖
function traverse(value, seen = new Set()) {
    // 如果要读取的数据是原始值, 或者已经被读取过了, 那么什么都不做
    if (typeof value !== 'object' || value === null || seen.has(value)) return

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

    // 暂时不考虑数组等其他数据结构
    // 假设value就是一个对象, 使用for...in读取对象地每一个值,并递归地调用traverse进行处理
    for (const k in value) {
        traverse(value[k], seen)
    }

    return value
}

watch接受getter函数

watch函数除了直接传递一个响应式对象的用法,还有传递一个getter函数的用法,如下:

javascript 复制代码
watch(() => proxyObj.foo, () => {
    console.log('proxyObj 里的数据变动了');
})

我们在watch函数的内部做一下传递进来的是否是函数的区分:

javascript 复制代码
function watch(source, cb) {
    let getter
    
    // 区分传递进来的是函数还是对象
    if (typeof source === 'function') {
        getter = source
    } else {
        getter = () => traverse(source)
    }

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

watch存储旧值

watch还有一个很重要的特性,在回调函数中我们应该能够拿到新值旧值,例如下面这样:

javascript 复制代码
watch(() => proxyObj.foo, (newValue, oldValue) => {
    console.log(newValue, oldValue, 'proxyObj 的foo属性数据变动了');
})

我们需要存储旧值和新值,为了实现这一特性,就需要用到我们上一篇关于computed实现中的懒执行,我们先来回顾一下:

javascript 复制代码
function effect(fn, options = {}) {
    const effectFn = () => {
        cleanup(effectFn) // 新增
        activeEffectFn = effectFn
        // 在调用副作用函数之前将当前副作用函数压入栈中
        effectStack.push(effectFn)
        // 接受fn函数的返回值
        const res = fn()
        // 结束之后,弹出副作用函数
        effectStack.pop()
        // 把activeEffectFn改为栈顶的函数
        activeEffectFn = effectStack[effectStack.length - 1]
        return res
    }
    effectFn.deps = []
    // 将 options 挂载到 effectFn 上
    effectFn.options = options // 新增
    if (!options.lazy) {
        effectFn()
    }
    // 把副作用函数返回
    return effectFn
}
  1. 添加了lazy:true 选项后,会将副作用函数返回,我们可以手动执行副作用函数以拿到返回值res,这个就是我们需要存储的旧值;
  2. 新值则在执行到调度器scheduler时重新调用副作用函数拿到。

我们按这个思路修改watch函数的内部实现:

diff 复制代码
function watch(source, cb) {
    let getter
    // 定义新值和旧值
+   let oldValue, newValue
    if (typeof source === 'function') {
        getter = source
    } else {
        getter = () => traverse(source)
    }

-   // effect(() => getter(), {
   // 获取返回的副作用函数 
+   const effectFn = effect(() => getter(), {
       // 添加lazy:true选项
+       lazy: true,
        scheduler() {
-            // cb()
            // 重新执行副作用函数,拿到新值
+            newValue = effectFn()
            // 传递给回调函数旧值和新值
+            cb(newValue, oldValue)
            // 存储为旧值
+            oldValue = newValue
        }
    })

    // 手动调用副作用函数,拿到值
+    oldValue = effectFn()
}

立即执行的watch

默认情况下,watch的回调需要监听的值有改动才会执行,但在Vue.js中,我们可以传递immediate: true来让回调函数立即执行,修改watch内部实现如下:

javascript 复制代码
function watch(source, cb, options = {}) {
    let getter
    //定义新值和旧值
    let oldValue, newValue
    if (typeof source === 'function') {
        getter = source
    } else {
        getter = () => traverse(source)
    }
    
    // 将调度器提取出来为一个函数job
    const job = () => {
        // 重新执行副作用函数,拿到新值
        newValue = effectFn()
        // 传递给回调函数旧值和新值
        cb(newValue, oldValue)
        // 存储为旧值
        oldValue = newValue
    }

    // 获取返回的副作用函数 
    const effectFn = effect(() => getter(), {
        // 添加lazy:true选项
        lazy: true,
        scheduler: job
    })
    
    // 传递了立即执行,则调用job函数
    if (options.immediate) {
        job()
    } else {
        oldValue = effectFn()
    }
}

修改实现中把调度器 独立成了一个job函数,以便我们可以手动执行,如果传递给watch的选项选项中有immediate: true则立即执行调度器,这也符合Vue中如果立即执行,oldValue是undefined

小结

这一节中我们实现了一个基本的watch函数,我们可以对整个对象的所有属性监听,也兼容了传递一个getter的形式,可以在回调中拿到旧值和新值,并且可以选择是否要立即执行回调。有什么写的不对的地方或者什么疑问可以留言,希望文章有帮助到你~

参考书籍

《Vue.js设计与实现》------------霍春阳

相关推荐
To_OC1 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic2 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy2 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志2 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble3 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯4 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜5 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司5 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
江华森6 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http
IT_陈寒6 小时前
Redis的KEYS命令把我搞崩溃了,改用SCAN才活过来
前端·人工智能·后端