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

关键词:watch()javascript

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

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

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

watch() 接收的第一个参数被称做 “数据源”,它能够是:后端

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

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

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

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

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

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

说完这个,不知道你还记不记得前面提到的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以后,就会按照正常顺序获取,先后天然就不同了。

目录

Vue 3.0 function-based API尝鲜(一):前言

Vue 3.0 function-based API尝鲜(二):配置与启动

Vue 3.0 function-based API尝鲜(三):包装对象

Vue 3.0 function-based API尝鲜(五):生命周期

Vue 3.0 function-based API尝鲜(六):组件间通讯

Vue 3.0 function-based API尝鲜(七):This与Refs