技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

优雅回答 watch 和 computed 的区别和选择?

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/61-优雅回答watch和computed的区别和选择?
本文目录8 个章节
  1. 一、面试官想考察什么
  2. 二、computed 和 watch 的区别
  3. 三、如何选择
  4. 四、常用细节
  5. 五、Vue 2.7 与 Vue 3.5 的 API 差异
  6. 六、computed 的原理
  7. 七、面试版总结
  8. 参考资料

优雅回答 watchcomputed 的区别和选择?

原文主要以 Vue 2 Options API 为背景。下面保留“定义—场景—细节—原理”的面试回答结构,并补充 Vue 2.7、Vue 3.5 的 API 差异。

一、面试官想考察什么

这个问题通常可以从四个方面回答:

  1. computedwatch 的定义及使用差异;
  2. 不同场景下如何选择;
  3. 缓存、深度监听、异步清理等细节;
  4. Vue 2 和 Vue 3 的 API、响应式实现差异。

二、computedwatch 的区别

1. computed:从状态得到派生值

计算属性用于根据已有响应式状态计算出另一个值。它应该尽量是纯函数,不负责请求接口、修改其他状态或执行日志等副作用。

<script setup lang="ts">
import { computed, ref } from 'vue'

const firstName = ref('周')
const lastName = ref('杰伦')

const fullName = computed(() => `${firstName.value}${lastName.value}`)
</script>

<template>
  <p>{{ fullName }}</p>
</template>

computed 默认具有缓存性:依赖没有变化时,重复读取会复用上一次的结果。它和 methods 的区别可以这样回答:

<script setup lang="ts">
import { computed, ref } from 'vue'

const count = ref(1)
const doubleCount = computed(() => count.value * 2)

function getDoubleCount() {
  return count.value * 2
}
</script>

<template>
  <!-- doubleCount 在依赖不变时可以复用缓存 -->
  <p>{{ doubleCount }}</p>
  <!-- methods 每次渲染执行到这里都会调用函数 -->
  <p>{{ getDoubleCount() }}</p>
</template>

缓存不是“任何情况下都更快”。如果计算非常简单,缓存管理本身也有成本;是否使用应由语义和实际更新频率决定。

2. watch:监听变化并执行副作用

watch 需要明确指定监听源,当源变化后执行回调,适合:

  • 发起或取消异步请求;
  • 把状态同步到 localStorage、URL 或第三方库;
  • 记录日志、埋点;
  • 在一个状态变化后修改另一个状态;
  • 需要拿到新值和旧值的场景。
<script setup lang="ts">
import { ref, watch } from 'vue'

const keyword = ref('')

watch(keyword, async (newKeyword, oldKeyword) => {
  console.log({ newKeyword, oldKeyword })
  // 请求、持久化或其他副作用
})
</script>

watch 没有返回值”是不准确的。Vue 3 的 watch() 返回停止监听的函数,Vue 2 的 $watch() 也会返回 unwatch 函数:

const stop = watch(keyword, () => {
  // ...
})

stop()

watch 的回调可以是异步函数,但 watch 本身不是“异步版 computed”,它的核心区别仍然是副作用派生值

三、如何选择

可以用一句话概括:

要得到一个值,优先考虑 computed;状态变化后要做事情,使用 watch;只想自动使用当前依赖而不关心旧值,可以考虑 watchEffect

适合 computed 的场景

const activeUsers = computed(() => {
  return users.value.filter(user => user.active)
})
  • 过滤、排序、格式化列表;
  • 计算总价、全名、权限结果;
  • 根据多个状态得到一个模板使用的值;
  • 需要缓存的同步派生数据。

适合 watch 的场景

watch(
  () => route.params.id,
  id => loadArticle(id),
  { immediate: true },
)
  • 路由参数变化后重新请求;
  • 搜索关键词变化后 debounce 请求;
  • 监听表单并保存草稿;
  • 与地图、图表、编辑器等非 Vue 对象同步。

不要为了“有变化就执行代码”而把所有逻辑都写成 watch。如果逻辑只是把 ab 计算为 ccomputed 更容易维护,也更少产生状态同步问题。

四、常用细节

1. 可读写 computed

computed 默认只有 getter,也可以显式提供 getter 和 setter:

import { computed, ref } from 'vue'

const firstName = ref('周')
const lastName = ref('杰伦')

const fullName = computed({
  get: () => `${firstName.value}${lastName.value}`,
  set: value => {
    const [nextFirstName, ...rest] = value.trim().split(' ')
    firstName.value = nextFirstName
    lastName.value = rest.join(' ')
  },
})

Options API 写法:

export default {
  data: () => ({
    firstName: '周',
    lastName: '杰伦',
  }),
  computed: {
    fullName: {
      get() {
        return `${this.firstName}${this.lastName}`
      },
      set(value) {
        // 根据业务规则拆分并更新 data
      },
    },
  },
}

setter 不是让 computed 变成一个独立状态,它仍然应该把写操作转发到真实状态。

2. immediate

默认情况下,watch 回调只在监听源变化后执行;immediate: true 会在创建监听时先执行一次:

watch(
  () => props.id,
  id => loadArticle(id),
  { immediate: true },
)

3. deep

监听对象的某个嵌套变化时,可以使用深度监听:

watch(
  form,
  value => saveDraft(value),
  { deep: true },
)

深度监听需要遍历或追踪大量嵌套依赖,数据树很大时可能产生明显开销。不要把“字符串形式监听”当成通用优化方案,更推荐精确监听真正需要的叶子路径:

watch(
  () => form.user.name,
  name => saveName(name),
)

Vue 3.5 还支持把 deep 设置为数字,限制遍历深度:

watch(source, callback, { deep: 2 })

数字深度适合有明确数据层级的场景,仍然需要结合数据规模测试。

4. watchEffect

watchEffect 会立即执行一次,并自动收集回调同步执行期间访问的响应式依赖:

import { watchEffect } from 'vue'

const stop = watchEffect(() => {
  document.title = `${count.value} 条消息`
})

它适合依赖较少且不需要旧值的副作用。需要精确控制监听源、immediatedeep 或新旧值时,使用 watch 更清晰。

watchEffect 的异步回调只会自动跟踪第一个 await 之前访问的依赖:

watchEffect(async () => {
  const id = currentId.value // 会被跟踪
  await fetch(`/api/articles/${id}`)
  // await 之后新读取的响应式值不会作为本次自动依赖
})

5. 执行时机和清理

Vue 3 的 watcher 默认会在组件更新前批量执行,可以通过 flush 调整时机:

watch(source, callback, { flush: 'post' })
watchSyncEffect(() => {
  // 同步执行,使用前要谨慎
})

flush: 'post' 适合需要读取组件更新后 DOM 的场景;但复杂 DOM 交互通常仍建议在逻辑中明确使用 await nextTick()

异步请求需要处理竞态和清理。Vue 3.5 可以使用 onWatcherCleanup,它必须在异步回调第一次 await 之前注册:

import { onWatcherCleanup, watch } from 'vue'

watch(searchText, async value => {
  const controller = new AbortController()
  onWatcherCleanup(() => controller.abort())

  const response = await fetch(`/api/search?q=${value}`, {
    signal: controller.signal,
  })
  results.value = await response.json()
})

也可以使用回调的第三个参数 onCleanup,它对需要兼容较早 Vue 3 版本的代码更友好:

watch(source, async (value, oldValue, onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort())
  // ...
})

五、Vue 2.7 与 Vue 3.5 的 API 差异

Vue 2.7 Options API

export default {
  data: () => ({
    firstName: '周',
    lastName: '杰伦',
    keyword: '',
  }),
  computed: {
    fullName() {
      return `${this.firstName}${this.lastName}`
    },
  },
  watch: {
    keyword: {
      immediate: true,
      handler(value, oldValue) {
        // 执行副作用
      },
    },
    // Vue 2 支持简单的点分隔路径,不支持任意表达式
    'user.profile.name'(value) {
      // ...
    },
  },
}

Vue 3 Composition API

import { computed, ref, watch } from 'vue'

const firstName = ref('周')
const lastName = ref('杰伦')
const keyword = ref('')

const fullName = computed(() => `${firstName.value}${lastName.value}`)

watch(keyword, value => {
  // 副作用
})

watch(
  () => user.profile.name,
  value => {
    // Composition API 使用 getter 表达路径
  },
)

Vue 3 Composition API 的 watch source 不是任意字符串表达式。原文“Vue 3 去掉了点操作符之外的字符串形式”需要改成:Vue 2 的 $watch/Options API 可以使用有限的简单路径;Composition API 应传入 ref、reactive 对象、getter 或 source 数组,不能把任意字符串当作表达式解析。

Vue 2.7 回移了部分 Composition API,但其底层响应式仍属于 Vue 2 的 getter/setter 体系;不能因为都能写 watch() 就认为两代 watcher 内部完全相同。

六、computed 的原理

6.1 Vue 2.7:lazy Watcher 和 Dep

Vue 2 的响应式链路可以简化为:

Observer/defineReactive
        ↓ 读取时收集
        Dep
        ↓ 通知
      Watcher

computed 的 watcher 具有 lazy/dirty 语义:

  1. 创建 computed watcher 时不立即执行 getter;
  2. 第一次访问 computed 时执行 getter,收集它依赖的响应式属性;
  3. 依赖变化后将 watcher 标记为 dirty;
  4. 下一次访问时重新计算并缓存;
  5. 外层渲染 watcher 读取 computed 时,computed 还会把依赖通知转发出去。

因此,“computed 在 data 的 setter 中直接重新计算”是不准确的。setter 负责通知依赖,computed 通常在下一次访问时惰性求值。

6.2 Vue 3.5:ComputedRef 和响应式 effect

Vue 3 的 computed() 返回一个 ref-like 对象:读取 .value 时进行依赖收集,依赖变化时让 computed 失效或触发调度,下一次读取时再按需求值。其具体内部类和 Dep/Link 结构属于实现细节,不应把 Vue 2 的 Watcher 源码直接套到 Vue 3。

七、面试版总结

computed 用于从响应式状态得到派生值,getter 应保持纯净,并且具有缓存;watch 用于监听明确的响应式源,在变化后执行副作用,可以拿到新旧值、执行异步逻辑并配置 immediate/deep/flush 等选项。Vue 2.7 常见实现是 Observer、Dep 和 lazy Watcher;Vue 3.5 使用 Composition API 的 computedwatchwatchEffect,对象响应式底层主要基于 Proxy。需要“算出一个值”选 computed,需要“发生变化后做事情”选 watch。

参考资料

原文作者:Dignity_呱。原文主题和面试问答结构予以保留,版本边界、watch 清理和 computed 原理已按 Vue 2.7/Vue 3.5 重新整理。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS