返回

Vue3 中 watch、watchEffect、computed 怎么选

用一个订单和搜索列表的小例子,整理 Vue3 中 computed、watch、watchEffect 的适用场景,以及避免重复请求和副作用失控的写法。

刚开始写 Vue3 时,这三个 API 很容易混着用。它们都和响应式数据有关,但处理的问题其实不同:computed 用来推导数据,watch 用来处理明确的变化,watchEffect 适合自动收集依赖的副作用。

先记住这个选择方式

  • 页面上需要一个由已有数据计算出来的新值,用 computed
  • 某个值变化后需要请求接口、保存草稿或同步第三方状态,用 watch
  • 一开始不确定依赖有哪些,并且希望代码立即执行一次,用 watchEffect

不要为了“监听数据”而一律使用 watch。如果只是把数量和单价相乘得到金额,直接计算更清晰。

用 computed 推导页面数据

下面这个小计只依赖数量和单价。它没有请求接口,也没有修改外部状态,因此最适合用 computed

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<script setup lang="ts">
import { computed, ref } from 'vue'

const count = ref(2)
const unitPrice = ref(39.9)

const totalPrice = computed(() => {
  return Number((count.value * unitPrice.value).toFixed(2))
})
</script>

<template>
  <p>商品数量{{ count }}</p>
  <p>订单小计{{ totalPrice }} </p>
</template>

computed 会缓存结果。只有 countunitPrice 改变时,totalPrice 才会重新计算。模板里可以直接使用 totalPrice,不需要 .value

用 watch 处理可控的副作用

搜索关键词变化后请求列表,是典型的 watch 场景。它的好处是监听来源明确,也能在新请求开始前清理旧请求。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { ref, watch } from 'vue'

const keyword = ref('')
const list = ref<string[]>([])

watch(keyword, async (value, _oldValue, onCleanup) => {
  const controller = new AbortController()

  onCleanup(() => controller.abort())

  try {
    const response = await fetch(
      `/api/users?keyword=${encodeURIComponent(value)}`,
      { signal: controller.signal },
    )

    if (!response.ok) return
    list.value = await response.json()
  } catch (error) {
    if ((error as Error).name !== 'AbortError') throw error
  }
})

用户连续输入时,前一次请求可能比后一次更晚返回。上面的 AbortController 会在关键词再次变化时取消旧请求,避免旧结果覆盖新结果。

如果页面打开时也要先加载一次,可以传入第三个配置参数:

1
watch(keyword, loadUsers, { immediate: true })

用 watchEffect 快速同步依赖

watchEffect 会立即运行,并在执行过程中自动收集它读到的响应式数据。比如把编辑状态保存到本地草稿:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { reactive, watchEffect } from 'vue'

const draft = reactive({
  title: '',
  content: '',
})

watchEffect(() => {
  localStorage.setItem('article-draft', JSON.stringify(draft))
})

这里不需要手动写 [draft.title, draft.content]。不过依赖来源越隐蔽,后期越难排查,因此涉及请求、路由跳转或复杂业务时,我更倾向于用 watch 写清监听对象。

常见误区

computed 不适合写异步请求。它应该保持纯粹,只返回由现有状态推导出的结果。

watchEffect 不适合替代所有 watch。它会自动追踪执行期间访问过的响应式数据,代码稍复杂时可能会触发比预期更多的更新。

监听对象时也要注意层级。watch(user, callback) 监听 reactive 对象会触发深层变化;如果只关心用户名,就写成 watch(() => user.name, callback),范围更准确。

小结

把三者分开后,代码会更好读:页面展示值交给 computed,明确的业务变化交给 watch,简单的自动同步交给 watchEffect。这也是我在 Vue3 项目里判断响应式逻辑该放在哪里的第一步。

使用 Hugo 构建
主题 StackJimmy 设计