欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Vue 3.0 function-based API尝鲜(四):值得一提的watch

程序员文章站 2022-05-17 07:58:18
...

关键词:watch()

因为watch的用法相对来说比较特殊,而且也是我第一个尝试的API,所以单独拿出来说一说。需要明确的是,watch是“副作用”,它只是在状态变化时的附加效果而已,并不是函数/状态的返回值。

先来看看用法。其实用法和2.x里命令式的vm.$watch( expOrFn, callback, [options] )用法差不多,也大致是这个结构。用TypeScript的类型来表示的话,就是:watch(source: Wrapper | () => any, callback: (newVal, oldVal), options?: WatchOption): Function。插一句,我觉得这个接口很优雅。变化在哪?第一个参数变成了“数据源”,选项也有所变化。至于watch为什么会有返回值,其实跟2.x是一致的:手动取消监听。显然,组件销毁的时候一定会取消监听,但如果监听只能在销毁的时候取消,就有点僵硬了。所以,这个返回值还是很有必要的,能实现更细粒度的控制。

先说说第一个参数,也就是“数据源”。之前提过,数据源是函数、包装对象和包含前两者的数组。不过为了准确,还是用尤大的原话吧:

watch() 接收的第一个参数被称作 “数据源”,它可以是:

  • 一个返回任意值的函数
  • 一个包装对象
  • 一个包含上述两种数据源的数组

其实就是之前的TypeScript接口里的类型。具体用法大概是这样的:

watch(
  () => foo.value,
  // 或者是value('bar')
  // 或者是类似于[() => foo.value, value('bar')]这样的数组
  (newVal, oldVal) => {// do sth...},
  {// 可能存在的选项}
)

在监听数组的时候,有一个地方或许需要注意一下。其实2.x的文档里已经提到了:

在变异 (不是替换) 对象或数组时,旧值将与新值相同,因为它们的引用指向同一个对象/数组。Vue 不会保留变异之前值的副本。

这里指的是数组的引用不会发生变化,而不是newVal和oldVal相同。这个可以在demo里看到。

说完这个,不知道你还记不记得前面提到的props的问题。props并不能直接作为watch()的数据源;虽然它是一个“可响应的对象“。因为它不是包装对象,所以无论是监听整个props,还是props里的属性,都会报错。这一点需要注意。也就是说,这样的写法是错误的:

watch(
    () => props.foo,
    (newVal, oldVal) => {}
)

那么,怎么解决呢?参考2.x的文档:

**这个 prop 以一种原始的值传入且需要进行转换。**在这种情况下,最好使用这个 prop 的值来定义一个计算属性。

所以,我们可以这么写,用computed包装一下,然后用watch去监听变化:

const localFoo = computed(() => props.foo)
watch(
    localFoo,
    (newVal, oldVal) => {// do sth...}
)

所以我说计算属性(其实应该叫计算值,Computed Value,但一下子改不过来)是个好东西,在整个Vue的使用过程中都发挥着重要的作用。

不过,按照3.0的思路,还有更简单的写法。我们可以只用函数简单包装一下:

watch(
    () => props.foo, // 监听不到变化
    (newVal, oldVal) => {// do sth...}
)

此外,可能你还会注意到一个奇怪的现象。watch在页面刚挂载的时候,往往会报错,而且通常是"undefined"“xxx is not a function”这种因为数据没加载完就渲染导致的错误。看起来很奇怪,不过看看尤大的说法,也说得过去:

和 2.x 的 $watch 有所不同的是,watch() 的回调会在创建时就执行一次。这有点类似 2.x watcher 的 immediate: true 选项,但有一个重要的不同:默认情况下 watch() 的回调总是会在当前的 renderer flush 之后才被调用 —— 换句话说,watch()的回调在触发时,DOM 总是会在一个已经被更新过的状态下。 这个行为是可以通过选项来定制的。

在 2.x 的代码中,我们经常会遇到同一份逻辑需要在 mounted 和一个 watcher 的回调中执行(比如根据当前的 id 抓取数据),3.0 的 watch() 默认行为可以直接表达这样的需求。

经过测试,watch的回调函数的触发甚至在onCreate()之前;此时页面必然没有渲染完成,报错就很正常了。这样的好处,大概就像他说的一样,能直接处理从后端拿数据这样的业务场景。但是这个报错真的很烦人……所以,watch也提供了选项,来调整watch回调的触发时机,也就是lazy。全部的选项是这样的:

interface WatchOptions {
  lazy?: boolean
  deep?: boolean
  flush?: 'pre' | 'post' | 'sync'
  onTrack?: (e: DebuggerEvent) => void
  onTrigger?: (e: DebuggerEvent) => void
}

事实上,onTrackonTrigger还没有实现。

Vue 3.0在这里的调整,主要就是调整了watch的默认行为,顺便增加了一些方便追踪和调试的功能。

当然了,光说未免有点不实在,可以看看demo。这个demo里有一个很有趣的问题,值得思考一下。为什么两个watcher通过ref获取的DOM结点值不相同?其实这个说起来也简单。还记得Vue的异步更新队列吗?

可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

前面提到,“默认情况下 watch() 的回调总是会在当前的 renderer flush 之后才被调用”,相当于默认情况下隐式地调用了$nextTick,会在DOM更新完成后才获取DOM值(因为在事件队列为空之前后面的操作是插不进去的),也就出现了前后获取的DOM值相同的情况;而加了lazy之后,就会按照正常顺序获取,前后自然就不一样了。