Vue 3 响应式与组件设计
会用 ref 和 computed 只是起点。复杂页面真正的问题是:状态放在哪里、更新由谁触发、依赖何时释放,以及如何避免无意义的深层响应式成本。
响应式的核心模型
Vue 3 在读取响应式数据时收集当前副作用,在写入时触发相关副作用。reactive 通过 Proxy 代理对象,ref 用带 .value 的容器统一承载基本类型和对象;模板会自动解包 ref。
import { computed, reactive, ref, shallowRef } from 'vue'
const query = reactive({ keyword: '', page: 1 })
const rows = shallowRef<Row[]>([])
const selectedIds = ref(new Set<string>())
const selectedCount = computed(() => selectedIds.value.size)这里用 shallowRef 保存服务端返回的大列表,是因为页面通常替换整个数组,不需要让每个深层字段都成为响应式代理。优化之前仍应通过 Vue DevTools 和 Performance 录制确认更新瓶颈。
ref、reactive 与 shallow 系列怎么选
- 独立值、可能整体替换的对象:优先
ref,API 一致且解构边界清楚。 - 聚合表单:
reactive可读性好,但不要直接解构;需要解构时使用toRefs。 - 大型只读结构、第三方实例:
shallowRef或markRaw,显式控制更新。 - 派生数据:
computed;不要用watch手工同步一份可计算状态。
watch 的正确职责
watch 适合“状态变化后执行副作用”,例如请求、存储和第三方 SDK。快速输入会产生竞态,需要清理过期任务。Vue 3.5 提供 onWatcherCleanup;跨版本代码也可使用回调参数中的 onCleanup。
watch(
() => query.keyword,
async (keyword, _, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
rows.value = await search(keyword, { signal: controller.signal })
},
)避免 watch(route) 或对大型对象做无限制 deep: true。监听具体参数,既减少遍历成本,也让触发条件更容易测试。
组件抽象看“变化轴”
我通常按三层拆分:基础 UI 组件负责交互与可访问性,领域组件表达业务语义,页面组件负责编排数据流。公共组件至少要满足:稳定复用需求、清晰变化轴、可测试边界。
<ProductSelector
v-model="selectedIds"
:source="productSource"
:permission="permission"
@confirm="handleConfirm"
>
<template #empty>当前筛选条件下没有商品</template>
</ProductSelector>Props/Emits 表达数据与行为契约,slot 处理结构变化。不要为了“通用”堆几十个布尔属性;如果两个场景的业务语义和生命周期已经不同,保留两个领域组件更清楚。
性能排查顺序
- 先判断是网络、JavaScript、布局还是组件更新。
- 用 Vue DevTools 检查重复渲染和依赖范围,用 Performance 看 Long Task/Layout。
- 大列表再考虑虚拟化、稳定 key、稳定 props 和按需加载。
- 对优化前后记录相同交互、相同数据量的对比。
追问
为什么子组件多了可能变慢? 组件实例与响应式 effect 有成本,长列表中过度抽象会被成倍放大。
v-if 和 v-show 怎么选? 低频切换用 v-if 避免常驻实例,高频切换用 v-show 避免反复挂载;还要看初始化成本和隐藏内容的可访问性。
defineModel 的价值? Vue 3.4 起可声明组件 v-model,减少 prop/emit 样板,但模型命名、修饰符与更新边界仍需显式设计。