《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设计与实现》------------霍春阳

相关推荐
软件技术NINI几秒前
HTML——基本标签
前端·javascript·html
卡兰芙的微笑24 分钟前
get_property --Cmakelist之中
前端·数据库·编辑器
覆水难收呀27 分钟前
三、(JS)JS中常见的表单事件
开发语言·前端·javascript
猿来如此呀34 分钟前
运行npm install 时,卡在sill idealTree buildDeps没有反应
前端·npm·node.js
hw_happy40 分钟前
解决 npm ERR! node-sass 和 gyp ERR! node-gyp 报错问题
前端·npm·sass
FHKHH44 分钟前
计算机网络第二章:作业 1: Web 服务器
服务器·前端·计算机网络
计算机程序设计开发1 小时前
计算机毕业设计公交站点线路查询网站登录注册搜索站点线路车次/springboot/javaWEB/J2EE/MYSQL数据库/vue前后分离小程序
数据库·vue.js·spring boot·课程设计·计算机毕业设计
QQ13049796941 小时前
Vue+nodejs+express旅游景区门票预订网站的设计与实现 8caai前后端分离
vue.js·express·旅游
视觉小鸟1 小时前
【JVM安装MinIO】
前端·jvm·chrome
二川bro2 小时前
【已解决】Uncaught RangeError: Maximum depth reached
前端