Skip to content

Vue 3 响应式与组件设计

会用 refcomputed 只是起点。复杂页面真正的问题是:状态放在哪里、更新由谁触发、依赖何时释放,以及如何避免无意义的深层响应式成本。

响应式的核心模型

Vue 3 在读取响应式数据时收集当前副作用,在写入时触发相关副作用。reactive 通过 Proxy 代理对象,ref 用带 .value 的容器统一承载基本类型和对象;模板会自动解包 ref。

ts
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
  • 大型只读结构、第三方实例:shallowRefmarkRaw,显式控制更新。
  • 派生数据:computed;不要用 watch 手工同步一份可计算状态。

watch 的正确职责

watch 适合“状态变化后执行副作用”,例如请求、存储和第三方 SDK。快速输入会产生竞态,需要清理过期任务。Vue 3.5 提供 onWatcherCleanup;跨版本代码也可使用回调参数中的 onCleanup

ts
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 组件负责交互与可访问性,领域组件表达业务语义,页面组件负责编排数据流。公共组件至少要满足:稳定复用需求、清晰变化轴、可测试边界。

vue
<ProductSelector
  v-model="selectedIds"
  :source="productSource"
  :permission="permission"
  @confirm="handleConfirm"
>
  <template #empty>当前筛选条件下没有商品</template>
</ProductSelector>

Props/Emits 表达数据与行为契约,slot 处理结构变化。不要为了“通用”堆几十个布尔属性;如果两个场景的业务语义和生命周期已经不同,保留两个领域组件更清楚。

性能排查顺序

  1. 先判断是网络、JavaScript、布局还是组件更新。
  2. 用 Vue DevTools 检查重复渲染和依赖范围,用 Performance 看 Long Task/Layout。
  3. 大列表再考虑虚拟化、稳定 key、稳定 props 和按需加载。
  4. 对优化前后记录相同交互、相同数据量的对比。

追问

为什么子组件多了可能变慢? 组件实例与响应式 effect 有成本,长列表中过度抽象会被成倍放大。

v-ifv-show 怎么选? 低频切换用 v-if 避免常驻实例,高频切换用 v-show 避免反复挂载;还要看初始化成本和隐藏内容的可访问性。

defineModel 的价值? Vue 3.4 起可声明组件 v-model,减少 prop/emit 样板,但模型命名、修饰符与更新边界仍需显式设计。

参考:Vue 响应式深入Watchers性能最佳实践