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

关键词: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之后,就会按照正常顺序获取,前后自然就不一样了。

目录

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

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值