Vue3 | 原来watch、watchEffect功能如此强大!

243 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情

原来以为自己会用 watch 、 watchEffect 了,最近我把 Vue3 的侦听器重新梳理了一下,发现只是略懂皮毛-_-,分享给大家,起来!

watch

1.基本用法

<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('This is answer. ;-)')

// 侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
  answer.value = 'wait a moment...'
  const res = await fetch('https://...')
  answer.value = (await res.json()).answer
})
</script>

<template>
  question:<input v-model="question" />
  <p>{{ answer }}</p>
</template>

watch() 一共可以接受三个参数,侦听数据源、回调函数、配置选项(可设置深度侦听deep、即时回调immediate)。

2. 侦听数据源

watch 的第一个参数可以是不同形式的“数据源”,它可以是:

  • 一个 ref(包括计算属性)
  • 一个 getter 函数(有返回值的函数)
  • 一个响应式对象
  • 多个数据源组成的数组(以上类型的值组成的数组)

注意,你不能直接侦听响应式对象的属性值,例如:

const obj = reactive({ count: 0 })

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`count is: ${count}`)
})
复制代码

这里需要用一个返回该属性的 getter 函数:

// 提供一个 getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)

3.回调函数

watch 的第二个参数是数据发生变化时执行的回调函数。

这个回调函数接受三个参数:value、oldValue和onCleanUp。第三个参数onCleanUp,用于注册副作用清理的回调函数, 在副作用下次执行之前,这个回调函数会被调用,通常用来清除不需要的或者无效的副作用,如等待中的异步请求:

const id = ref(1)
const data = ref(null)

watch(id, async (newValue, oldValue, onCleanup) => {
  const { response, cancel } = doAsyncWork(id.value)
  // `cancel` 会在 `id` 更改时调用
  // 以便取消之前未完成的请求
  onCleanup(cancel)
  data.value = await response.json()
})
复制代码
// 源码
// 副作用
export type WatchEffect = (onCleanup: OnCleanup) => void

export type WatchCallback<V = any, OV = any> = (
  value: V,
  oldValue: OV,
  onCleanup: OnCleanup
) => any

type OnCleanup = (cleanupFn: () => void) => void

watch 的返回值是一个用来停止该副作用的函数:

const unwatch = watch(() => {})

// ...当该侦听器不再需要时
unwatch()
复制代码

注意:使用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器。

如果使用异步回调创建一个侦听器,则不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。如下面例子:

<script setup>
import { watchEffect } from 'vue'

// 组件卸载会自动停止
watchEffect(() => {})

// 组件卸载不会停止!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

3.配置选项

watch 的第三个参数是一个可选的对象,支持以下选项:

  • immediate:在侦听器创建时立即触发回调(示例:请求一些初始数据,然后在相关状态更改时重新请求数据)。
  • deep:深度遍历,以便在深层级变更时触发回调(注意:深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能)
  • flush:回调函数的触发时机。pre:默认,dom 更新前调用,post: dom 更新后调用,sync 同步调用。
  • onTrack / onTrigger:用于调试的钩子。在依赖收集和回调函数触发时被调用。

watchEffect

watch() 是懒执行的:当数据源发生变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。而watchEffect() 恰好能做到,它会立即执行一遍回调函数,不需要向watch()使用 immediate 选项才能实现。

const url = ref('https://...')
const data = ref(null)

// 一个参数就可以搞定
watchEffect(async () => {
  const response = await fetch(url.value)
  data.value = await response.json()
})

watchEffect 接受两个参数,第一个参数是数据发生变化时执行的回调函数,用法和 watch 一样。第二个参数是一个可选的对象,支持 flushonTrack / onTrigger 选项,功能和 watch 相同。

注意:watchEffect 仅会在其同步执行期间,才追踪依赖。使用异步回调时,只有在第一个 await 之前访问到的依赖才会被追踪。

watch vs. watchEffect

watchwatchEffect 的主要功能是相同的,都能响应式地执行回调函数。它们的区别是追踪响应式依赖的方式不同:

  • watch 只追踪明确定义的数据源,不会追踪在回调中访问到的东西;默认情况下,只有在数据源发生改变时才会触发回调;watch 可以访问侦听数据的新值和旧值。
  • watchEffect 会初始化执行一次,在副作用发生期间追踪依赖,自动分析出侦听数据源;watchEffect 无法访问侦听数据的新值和旧值。

简单一句话,watch 功能更加强大,而 watchEffect 在某些场景下更加简洁。