前言
watch这个API大家应该都不陌生,在Vue3版本中给watch增加不少有用的功能,比如deep选项支持传入数字
、pause、resume、stop方法
、once选项
、onCleanup函数
。这些功能大家平时都不怎么用得上,但是在一些特定的场景中,他们能够起大作用,这篇文章欧阳就来带你盘点一下这些功能。
关注公众号:【前端欧阳】,给自己一个进阶vue的机会
deep
支持传入数字
deep
选项大家应该比较熟悉,常见的值为true
或者false
,表示是否深度监听watch
传入的对象。
在Vue3.5版本中对deep
选项进行了增强,不光支持布尔值,而且还支持传入数字,数字表示需要监听的层数。
比如下面这个例子:
const obj1 = ref({ a: { b: 1, c: { d: 2, e: { f: 3, }, }, }, }); watch( obj1, () => { console.log("监听到obj1变化"); }, { deep: 3, } ); function changeDeep3Obj() { obj1.value.a.c.d = 20; // 能够触发watch回调 } function changeDeep4Obj() { obj1.value.a.c.e.f = 30; // 不能触发watch回调 }
在上面的例子watch
的deep
选项值是3,表明监听到对象的第3层。
changeDeep3Obj
函数中就是修改对象的第3层的d
属性,所以能够触发watch
的回调。
而changeDeep4Obj
函数是修改对象的第4层的f
属性,所以不能触发watch
的回调。
他的实现也很简单,我们来看一下deep相关的源码:
function watch(source, cb, options) { // ...省略 if (cb && deep) { const depth = deep === true ? Infinity : deep getter = () => traverse(baseGetter(), depth) } // ...省略 }
这里的depth
就表示watch监听一个对象的深度。
如果deep
选项的值为true,那么就将depth
设置为正无穷Infinity
,说明需要监听到对象的最深处。
如果deep
选项的值为false,或者没有传入deep
,那么就表明只需要监听对象的最外层。
如果deep
选项的值为number类型数字,那么就把这个数字赋给depth
,表明需要监听到对象的具体某一层。
pause、resume、stop方法
这三个方法也是Vue3.5版本中引入的,通过解构watch
函数的返回值就可以直接拿到pause
、resume
、stop
这三个方法。
我们来看一下源码,其实很简单:
function watch(source, cb, options) { // ...省略 watchHandle.pause = effect.pause.bind(effect) watchHandle.resume = effect.resume.bind(effect) watchHandle.stop = watchHandle return watchHandle }
watch返回了一个名为watchHandle
的对象,对象上面有pause、resume、stop
这三个方法,所以我们可以通过解构watch
函数的返回值拿到这三个方法。
pause
方法的作用是“暂停”watch回调的触发,也就是说在暂停期间不管watch监听的响应式变量如何改变,他的回调函数都不会触发。
有“暂停”,那么肯定就有“恢复”。
resume
方法的作用是恢复watch回调的触发,此时会主动执行一次watch的回调。后面watch监听的响应式变量改变时,他的回调函数也会触发。
来看个demo,代码如下:
<template> <button @click="count++">count++</button> <button @click="runner.pause()">暂停</button> <button @click="runner.resume()">恢复</button> <button @click="runner.stop()">停止</button> </template> <script setup lang="ts"> import { watch, ref } from "vue"; const count = ref(0); const runner = watch(count, () => { console.log(count.value); }); </script>
点击“count++”按钮会导致watch
回调中的console执行。
但是当我们点击了“暂停”按钮后,此时我们再怎么点击“count++”按钮都不会触发watch
的回调。
点击恢复
按钮后会立即触发一次watch
回调的执行,后面点击“count++”按钮也同样会触发watch
的回调。
我们来看看pause
和resume
方法的源码,很简单,代码如下:
class ReactiveEffect { pause(): void { this.flags |= EffectFlags.PAUSED } resume(): void { if (this.flags & EffectFlags.PAUSED) { this.flags &= ~EffectFlags.PAUSED if (pausedQueueEffects.has(this)) { pausedQueueEffects.delete(this) this.trigger() } } } trigger(): void { if (this.flags & EffectFlags.PAUSED) { pausedQueueEffects.add(this) } else if (this.scheduler) { this.scheduler() } else { this.runIfDirty() } } }
在pause
、resume
方法中通过修改flags
属性的值,来切换是不是“暂停状态”。
在执行trigger
方法依赖触发时,就会先去读取flags
属性判断当前是不是“暂停状态”,如果是那么就不去执行watch的回调。
从上面的代码可以看到这三个方法是在ReactiveEffect
类上面的,这个ReactiveEffect
类是Vue的一个底层类,watch
、watchEffect
、watchPosEffect
、watchSyncEffect
都是基于这个类实现的,所以他们自然也支持pause
、resume
、stop
这三个方法。
最后就是stop
方法了,当你确定后面都不再想要触发watch的回调了,那么就调用这个stop
方法。代码如下:
const watchHandle: WatchHandle = () => { effect.stop() if (scope && scope.active) { remove(scope.effects, effect) } } watchHandle.stop = watchHandle
响应式变量count
收集的订阅者集合中有这个watch回调,所以当count
的值改变后会触发watch回调。这里的stop
方法中主要是依靠双向链表将这个watch回调从响应式变量count
的订阅者集合中给remove掉,所以执行stop方法后无论count
变量的值如何改变,watch回调也不会再执行了。(PS:如果你看不懂这段话,建议你去看看我的上一篇 Vue3.5双向链表文章,看完后你就懂了)
once选项
如果你只想让你的watch回调只执行一次,那么可以试试这个once
选项,这个是在Vue3.4版本中新加的。
看个demo:
<template> <button @click="count++">count++</button> </template> <script setup lang="ts"> import { watch, ref } from "vue"; const count = ref(0); watch( count, () => { console.log("once", count.value); }, { once: true, } ); </script>
由于使用了once
选项,所以只有第一次点击“count++”按钮才会触发watch的回调。后面再怎么点击按钮都不会触发watch回调。
我们来看看once
选项的源码,很简单,代码如下:
function watch(source, cb, options) { const watchHandle: WatchHandle = () => { effect.stop() if (scope && scope.active) { remove(scope.effects, effect) } } if (once && cb) { const _cb = cb cb = (...args) => { _cb(...args) watchHandle() } } // ...省略 watchHandle.pause = effect.pause.bind(effect) watchHandle.resume = effect.resume.bind(effect) watchHandle.stop = watchHandle return watchHandle }
先看中间的代码if (once && cb)
,这句话的意思是如果once
选项的值为true,并且也传入了watch回调。那么就封装一层新的cb
回调函数,在新的回调函数中还是会执行用户传入的watch回调。然后再去执行一个watchHandle
函数,这个watchHandle
是不是觉得有点眼熟?
前面讲的stop
方法其实就是在执行这个watchHandle
,执行完这个watchHandle
函数后watch就不再监听count
变量了,所以后续不管count
变量怎么修改,watch的回调也不会再触发。
onCleanup函数
有的情况我们需要watch监听一个变量,然后去发起http请求。如果变量改变的很快就会出现第一个请求还没回来,第二个请求就已经发起了。在一些极端情况下还会出现第一个请求的响应比第二个请求的响应还要慢,此时第一个请求的返回值就会覆盖第二个请求的返回值。实际上我们期待最终拿到的是第二个请求的返回值。
这种情况我们就可以使用onCleanup函数
,他是作为watch回调的第三个参数暴露给我们的。看个例子:
watch(id, async (newId, oldId, onCleanup) => { const { response, cancel } = myFetch(newId) // 当 `id` 变化时,`cancel` 将被调用, // 取消之前的未完成的请求 onCleanup(cancel) data.value = await response })
watch回调的前两个参数大家都很熟悉:新的id值和旧的id值。第三个参数就是onCleanup
函数,在watch回调触发之前调用,所以我们可以使用他来cancel掉上一次的请求。
onCleanup
函数的注册也很简单,代码如下:
let boundCleanup boundCleanup = fn => onWatcherCleanup(fn, false, effect) function watch(source, cb, options) { // ...省略 const job = (immediateFirstRun?: boolean) => { const args = [ newValue, oldValue, boundCleanup, ] cb(...args) oldValue = newValue } // ...省略 }
执行watch回调实际就是在执行这个job
函数,在job
函数中执行watch回调时传入了三个参数。分别是newValue
、oldValue
、boundCleanup
。前两个参数大家都很熟悉,第三个参数boundCleanup
是一个函数:fn => onWatcherCleanup(fn, false, effect)
。
这个onWatcherCleanup
大家熟悉不?这也是Vue暴露出来的一个API,注册一个清理函数,在当前侦听器即将重新运行时执行。关于onWatcherCleanup
之前欧阳写过一篇文章专门讲了如何使用: 使用Vue3.5的onWatcherCleanup封装自动cancel的fetch函数
总结
这篇文章盘点了Vue3 watch新增的一些新功能:deep选项支持传入数字
、pause、resume、stop方法
、once选项
、onCleanup函数
。这些功能大家平时可能用不上,但是还是要知道有这些功能,因为有的情况下这些功能能够派上大用场。
关注公众号:【前端欧阳】,给自己一个进阶vue的机会
另外欧阳写了一本开源电子书vue3编译原理揭秘,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。