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

显示模式

登录
ARCHIVE DOCUMENTVUE

前端面试怎么总问 watch 和 computed 的区别?

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/70-前端面试怎么总问watch和computed区别
本文目录8 个章节
  1. 一、先了解 Vue 3 响应式
  2. 二、Watch:监听明确 source,处理副作用
  3. 三、Computed:缓存的派生状态
  4. 四、源码层面的正确说法
  5. 五、watch 和 computed 的核心区别
  6. 六、Vue 2.7 和 Vue 3.5 的差异
  7. 七、面试建议
  8. 参考资料

前端面试怎么总问 watchcomputed 的区别?

watchcomputed 都依赖 Vue 的响应式系统,但它们不是两个“监听数据”的同义 API。面试时先讲职责,再讲缓存、调度、源码和版本差异,答案会比背源码更准确。

一、先了解 Vue 3 响应式

Vue 3 的响应式系统主要基于 ProxyReflect、依赖追踪和 scheduler:

读取 reactive/ref
       ↓ track
建立 effect 与状态的依赖
       ↓
写入状态
       ↓ trigger
调度组件更新、computed 或 watcher

Proxy 只是拦截入口,不等于完整的响应式系统。真正的更新还涉及依赖集合、effect 运行栈、清理旧依赖、批量调度和组件 renderer。

Vue 2.7 则主要使用 Object.definePropertyDepWatcher。因此,Vue 3 源码中不能简单地说“watch 创建了一个 Watcher 实例”;Vue 3 的内部抽象主要是响应式 effect 和 doWatch 等 watcher API 实现。

二、Watch:监听明确 source,处理副作用

1. 基本用法

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

const count = ref(0)
const doubledCount = ref(0)

watch(count, (newValue, oldValue) => {
  console.log({ newValue, oldValue })
  // 这里可以请求接口、记录日志或同步第三方对象
  doubledCount.value = newValue * 2
})
</script>

<template>
  <p>Count: {{ count }}</p>
  <p>Doubled: {{ doubledCount }}</p>
</template>

这个例子可以帮助理解 watch,但如果 doubledCount 只是 count 的派生值,生产代码应直接使用 computed:

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

watch() 返回停止监听的函数:

const stop = watch(count, () => {})
stop()

2. source 形式

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

watch(count, callback)
watch(() => state.user.name, callback)
watch([count, () => state.user.name], callback)
watch(state, callback)

直接把 reactive 对象作为 source 时,Vue 会进行深度观察;使用 getter 返回对象时,默认只在 getter 返回值变化时触发,是否深度追踪需要通过 deep 明确配置。

watch(
  () => state.user,
  user => {},
  { deep: true },
)

3. 调度、立即执行和清理

watch(
  () => route.params.id,
  id => loadArticle(id),
  {
    immediate: true,
    flush: 'post',
  },
)
  • immediate:创建 watcher 后先执行一次;
  • deep:递归追踪嵌套对象,可能有明显成本;
  • flush: 'pre':默认调度语义附近;
  • flush: 'post':组件 DOM 更新后执行;
  • flush: 'sync':同步执行,可能失去批量更新优势。

Vue 3.5 的异步清理:

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()
})

注册清理必须发生在第一次 await 之前;更早版本可以使用 watcher 回调的第三个 onCleanup 参数。

三、Computed:缓存的派生状态

import { computed, ref } from 'vue'

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

读取 doubledCount.value 时,computed getter 会按需运行并收集 count 依赖;依赖不变时再次读取会使用缓存。count 变化只会让 computed 失效,通常在下一次读取时重新计算,而不是在 setter 中直接执行 getter。

可写 computed:

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
  },
})

computed getter 应保持纯净,不应在其中发请求、修改其它响应式状态或操作 DOM。副作用应交给 watch/watchEffect 或事件处理函数。

四、源码层面的正确说法

1. Vue 3 watch 的源码思路

Vue 3 runtime-core 的 watcher API 会根据 source 创建 getter,然后创建响应式 effect;当依赖变化时由 scheduler 调度 job,job 再执行回调并比较新旧值。可以用以下伪代码表示:

function conceptualWatch(source, callback, options = {}) {
  const getter = createGetterFromSource(source, options)
  let oldValue = runEffect(getter)

  const job = () => {
    const newValue = runEffect(getter)
    if (options.deep || !Object.is(newValue, oldValue)) {
      callback(newValue, oldValue)
      oldValue = newValue
    }
  }

  scheduleEffect(getter, job, options.flush)

  if (options.immediate) job()
  return () => stopEffect(getter)
}

这不是 Vue 源码,真实实现还处理数组 source、深度遍历、cleanup、异步回调、递归更新和错误处理。Vue 3 源码中不应写成 new Watcher(vm, source, cb),那是 Vue 2 语境的简化表达。

2. Vue 3 computed 的源码思路

Vue 3 的 ComputedRefImpl 维护 getter、缓存值、dirty/失效状态和下游依赖。简化模型如下:

class ConceptualComputed {
  constructor(getter) {
    this.getter = getter
    this.dirty = true
    this.value = undefined
  }

  get value() {
    trackComputed(this)

    if (this.dirty) {
      this.value = runEffect(this.getter)
      this.dirty = false
    }

    return this.value
  }

  invalidate() {
    if (!this.dirty) {
      this.dirty = true
      triggerComputed(this)
    }
  }
}

原文把 createComputedComputedRefImplObject.defineProperty 混在一起。Vue 3 computed 返回的是 ref-like 对象,通过 .value 的 getter/setter 和响应式 effect 工作;Proxy 是 reactive 对象的主要拦截机制,但不能说 computed 本身“使用 Object.defineProperty 实现数据监听”。

五、watch 和 computed 的核心区别

维度computedwatch
目的得到派生值执行副作用
是否返回值返回 computed ref返回停止监听函数
缓存默认缓存派生结果不缓存副作用结果
sourcegetter 自动收集依赖明确指定 source,也可使用 watchEffect 自动收集
新旧值不以新旧值回调为核心可以拿到 newValue、oldValue
异步getter 应保持同步纯净常用于异步请求和清理
DOM 时机作为渲染数据使用可通过 flush 控制执行时机

选择口诀:

需要一个“值”,用 computed;变化之后要“做事”,用 watch;不想显式写 source 且不需要旧值,用 watchEffect。

六、Vue 2.7 和 Vue 3.5 的差异

Vue 2.7 Options API:

export default {
  data: () => ({
    count: 0,
  }),

  computed: {
    doubled() {
      return this.count * 2
    },
  },

  watch: {
    count: {
      immediate: true,
      handler(value, oldValue) {
        // 副作用
      },
    },
  },
}

Vue 3 Composition API:

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

watch(count, (value, oldValue) => {})
watchEffect(() => console.log(count.value))

Vue 2 的 $watch 和 Options API 可以使用有限的简单路径字符串;Vue 3 Composition API 应传入 ref、reactive 对象、getter 或 source 数组,不能把任意字符串当作表达式解析。Vue 2.7 虽然回移了部分 Composition API,但底层响应式实现仍然属于 Vue 2 体系。

七、面试建议

  1. 先说职责,不要一开始背源码;
  2. 解释 computed 的缓存和惰性求值;
  3. 解释 watch 的副作用、immediate、deep、flush 和 cleanup;
  4. 说明 Vue 2 的 Dep/Watcher 与 Vue 3 的 effect/scheduler 不同;
  5. 给一个“派生值用 computed、异步请求用 watch”的反例和正例;
  6. 不要把“性能优化”说成 computed 在任何场景都更快,仍需结合依赖数量和更新频率。

参考资料

原文作者:Bun。原文关于 Vue 3 响应式、源码分析、使用方式和面试建议予以保留,损坏的代码块、错误的 Watcher/Object.defineProperty 归因及版权推广内容已清理。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS