Vue 根据模板读取的响应式依赖更新界面。普通 JavaScript 对象不会因为出现在 script setup 中就自动变成响应式状态;要明确哪些值由 ref、reactive 等 API 管理,哪些只是常量和纯计算。

ref、reactive 与 computed

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

const count = ref(0);
const doubled = computed(() => count.value * 2);
</script>

<template>
  <button @click="count++">计数 {{ count }},两倍 {{ doubled }}</button>
</template>

脚本中 ref 通常通过 .value 访问,模板会在相应位置自动解包。reactive 返回对象代理;把代理里的原始值直接解构到普通变量,可能失去与原属性更新的联系,需要根据场景使用 toRef/toRefs 或保留属性访问。

computed 适合由已有状态得出的值,按依赖缓存结果;getter 应避免写状态和发请求。缓存不是无条件永久保存,依赖改变后会失效。过滤列表、统计数量可以计算得到,无需另存一份随时可能过期的副本。

父子边界

props 由父组件拥有,子组件通过 emit 表达变化请求。不要为了方便在子组件里直接改传入对象的深层字段;技术上可影响同一对象,不代表边界清晰。v-model 是值与更新事件的约定,用在输入组件时仍要说明校验、草稿和提交的时机。

列表使用稳定 key,避免排序和插入后错误复用输入状态。模板自动转义普通文本,但 v-html 会按 HTML 解释内容,需要额外安全处理。

什么时候使用 watch

watch 适合观察明确来源并执行副作用,例如查询变化后发起搜索;watchEffect 自动追踪执行时读取的响应式依赖,更适合依赖明确且规模小的同步逻辑。异步函数中 await 之后读取的依赖不能简单认为会被同样自动收集。

watch(query, async (value, _old, onCleanup) => {
  const controller = new AbortController();
  onCleanup(() => controller.abort());
  // 这里接入请求函数,并处理取消、错误与旧响应。
});

这是说明清理位置的接入片段,query 和 watch 需由组件提供。清理在启动异步工作时注册;不要等请求完成后才安排。组件卸载或监听来源变化后,不再需要的外部工作应能结束。修改响应式状态后的 DOM 更新通常会批处理,需要读取更新后的 DOM 时可 await nextTick,而不是固定延迟若干毫秒。

练习:先用 computed 实现待学数量,再故意改成 watch 复制一份数量,列出需要同步的路径。给搜索添加取消,验证旧错误不覆盖新结果。继续完成Vue 学习记录板。

面试自查:ref 与 reactive 如何选择?computed 和 watch 分别适合什么?为什么解构响应式对象可能丢失联系?

参考:响应式基础、计算属性、侦听器。