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

显示模式

登录
ARCHIVE DOCUMENTVUE

再也不怕面试官问 watch、computed、watchEffect 的区别了

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/68-再也不怕面试官问`watch`、`computed`、`watchEffect`的区别了
本文目录8 个章节
  1. 一、先给出面试结论
  2. 二、watch
  3. 三、computed
  4. 四、watchEffect
  5. 五、原理:不要把 Vue 2 的 Watcher 套到 Vue 3
  6. 六、三个 API 如何选择
  7. 七、面试版总结
  8. 参考资料

再也不怕面试官问 watchcomputedwatchEffect 的区别了

在 Vue 中,响应式系统会在读取状态时建立依赖关系,在状态写入后通知相关 effect 或组件更新。watchcomputedwatchEffect 都建立在这套机制之上,但职责不同:一个主要处理副作用,一个产生派生值,一个自动追踪副作用依赖。

一、先给出面试结论

  • computed:从已有响应式状态得到派生值,默认惰性求值并缓存,getter 应尽量保持纯净;
  • watch:监听明确的响应式 source,在变化后执行副作用,可以拿到新值和旧值;
  • watchEffect:立即执行一个副作用,并自动追踪同步执行期间访问的响应式依赖;
  • 需要“算出一个值”优先使用 computed;需要“变化后做事情”使用 watchwatchEffect

二、watch

1. 基本用法

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

const keyword = ref('')

const stop = watch(keyword, (newValue, oldValue) => {
  console.log('keyword:', oldValue, '→', newValue)
})

// 不再需要时可以停止监听
// stop()
</script>

Composition API 的 watch source 可以是:

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

const count = ref(0)
const state = reactive({ user: { name: 'Ada' } })

watch(count, value => {})
watch(() => state.user.name, (name, oldName) => {})
watch([count, () => state.user.name], ([nextCount, nextName]) => {})
watch(state, value => {}) // 直接监听 reactive 对象时默认深度追踪

Composition API 中不能把任意字符串当成路径表达式:

// ❌ 不是 Composition API watch 的通用写法
watch('user.name', callback)

// ✅ 用 getter 表达依赖
watch(() => state.user.name, callback)

Vue 2.7 的 Options API 或 $watch 仍可以监听有限的简单路径:

export default {
  watch: {
    'user.name'(value, oldValue) {
      // Vue 2 Options API 的简单路径写法
    },
  },
}

2. immediatedeepflush

watch(
  () => route.params.id,
  id => loadArticle(id),
  {
    immediate: true,
    flush: 'post',
  },
)
  • immediate: true:创建监听时先执行一次;
  • deep: true:递归观察嵌套变化,可能有较大开销;
  • flush: 'pre':默认调度语义附近,在组件 DOM 更新前执行;
  • flush: 'post':组件 DOM 更新后执行;
  • flush: 'sync':同步执行,可能失去批量更新好处,应谨慎使用。

Vue 3.5 支持数字形式的深度限制:

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

深度监听不是性能优化手段。数据树很大时,应尽量监听真正需要的叶子值或拆分状态。

3. 异步请求和清理

watch 适合执行异步副作用,但必须防止旧请求覆盖新请求:

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

const keyword = ref('')
const results = ref<unknown[]>([])

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

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

onWatcherCleanup 是 Vue 3.5 新增 API,必须在异步回调第一次 await 之前注册。为了兼容更早的 Vue 3 版本,也可以使用第三个参数:

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

watch() 会返回停止监听的函数,不应说成“没有返回值”。

三、computed

1. 派生数据和缓存

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

const count = ref(0)
const doubledCount = computed(() => count.value * 2)

const label = computed(() => `Count: ${count.value}`)
</script>

computed 的 getter 第一次读取时执行,之后会缓存结果;只有依赖发生变化,缓存才会失效,下一次读取时重新计算。如果依赖没有变化,多次读取可以复用同一个结果。

它适合:

  • 过滤、排序和格式化列表;
  • 计算总价、权限、全名等派生值;
  • 把复杂模板表达式提取到可复用的响应式值。

不要在 computed getter 中发请求、修改其他状态或执行日志等副作用:

// ❌ 不建议:getter 读取时可能被多次调用,副作用难以预测
const bad = computed(() => {
  saveToServer(state.value)
  return state.value.id
})

computed 的缓存是语义和性能特征,但不是任何情况下都更快。简单的 count + 1 用普通函数也可能足够,选择时首先看它是否是派生状态。

2. 可写 computed

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

const firstName = ref('Ada')
const lastName = ref('Lovelace')

const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: value => {
    const [first, last = ''] = value.split(' ')
    firstName.value = first
    lastName.value = last
  },
})

setter 不是让 computed 变成一份独立缓存状态,而是把写操作转发给真实的响应式状态。

四、watchEffect

1. 自动收集依赖

import { ref, watchEffect } from 'vue'

const count = ref(0)

const stop = watchEffect(() => {
  document.title = `Count: ${count.value}`
})

// stop()

watchEffect 会立即执行一次,执行过程中访问的响应式数据会被自动记录;这些数据变化后,effect 会重新执行。

watch 对比:

// watch:明确 source,只监听 count,能拿到新旧值
watch(count, (value, oldValue) => {
  console.log(value, oldValue)
})

// watchEffect:自动收集回调中读取的所有依赖,不提供新旧值
watchEffect(() => {
  console.log(count.value)
})

当依赖列表动态变化、且不需要旧值时,watchEffect 很方便;需要精确控制 source、immediatedeepflush 或异步竞态时,watch 更清晰。

2. 异步 effect 的边界

异步回调中,只有第一个 await 之前访问的响应式值会被自动追踪:

watchEffect(async () => {
  const id = currentId.value // 会被追踪
  await fetch(`/api/article/${id}`)

  // await 后新读取的响应式值不会作为本次自动依赖
})

需要将异步 source 和清理策略表达清楚时,通常使用 watch 更合适。

五、原理:不要把 Vue 2 的 Watcher 套到 Vue 3

1. Vue 2.7 的大致链路

Vue 2.7 主要使用 Object.definePropertyDepWatcher

响应式 getter 收集 Watcher
          ↓
响应式 setter 通知 Dep
          ↓
Watcher 入调度队列
          ↓
组件更新或执行 watch 回调

computed 使用 lazy watcher 和 dirty 标记:第一次读取时计算,依赖变化时标记失效,下一次读取时再计算。Vue 2 的 watcher 讲解可以使用下面的教学伪代码:

class Watcher {
  constructor(getter, callback) {
    this.getter = getter
    this.callback = callback
    this.value = this.get()
  }

  get() {
    pushTarget(this)
    const value = this.getter()
    popTarget()
    return value
  }

  update() {
    const oldValue = this.value
    const newValue = this.get()
    this.value = newValue
    this.callback(newValue, oldValue)
  }
}

这不是 Vue 2.7 可直接复制的源码,省略了依赖清理、深度遍历、队列去重和异常处理。

2. Vue 3.5 的大致链路

Vue 3 使用 Proxy/Reflect 及独立的 reactivity 包。watch 的实现位于 runtime-core 的 watcher API,底层会创建响应式 effect,并根据 source 生成 getter;当 effect 被触发时,scheduler 再按 flush 选项安排 job。

Vue 3 中不存在 Vue 2 语境下的 Watcher 类作为 watch 的核心。更准确的抽象是:

source getter / effect
        ↓ 读取时 track
响应式依赖图
        ↓ 写入时 trigger
scheduler 调度 job
        ↓
执行回调或重新运行 effect

computed 内部由 ComputedRefImpl 表示,它维护惰性求值和失效状态;访问 .value 时追踪 computed 自身,依赖变化时让缓存失效或触发下游 effect。它不是“数据变化时调用 computed setter”:setter 只有在使用者主动给可写 computed 赋值时才执行。

六、三个 API 如何选择

需求推荐 API原因
由状态计算列表、总价、标签computed派生值、缓存、可在模板中复用
状态变化后请求接口、写缓存、操作第三方库watch明确 source,支持新旧值和清理
自动使用当前 effect 中读取的依赖watchEffect不需要手动列出 source
只想同步两个字段的展示结果computed避免手动 watch 再写回第三份状态
需要 DOM 更新后运行副作用watch(..., { flush: 'post' })明确 DOM 时机

例如,下面的派生值不应使用 watch 维护第二份状态:

// ❌ 多维护了一份 doubledCount,存在同步风险
watch(count, value => {
  doubledCount.value = value * 2
})

// ✅ 直接使用 computed
const doubledCount = computed(() => count.value * 2)

七、面试版总结

computed 用于派生状态,getter 默认惰性执行并缓存,应该保持纯净;watch 用于监听明确的响应式 source,在变化后执行副作用,支持新旧值、immediate、deep、flush 和清理;watchEffect 会立即执行并自动追踪同步执行期间访问的依赖,适合不需要新旧值的简单副作用。Vue 2.7 的实现常用 Dep 和 Watcher,Vue 3.5 则使用 Proxy、ReactiveEffect 和 scheduler,不能把 Vue 2 的 Watcher 源码直接当作 Vue 3 实现。

参考资料

原文作者:Bun。原文关于三个 API 的定义、原理和选择场景予以保留,损坏的代码围栏、错误的 Vue 3 Watcher 源码描述、抓取噪声和版权推广内容已清理。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS