--- id: frontend.vue.reactivity-composition title: Vue 3 响应式原理、组合式函数、Pinia 和性能优化 type: lesson domain: frontend stack: [vue] tags: [Vue 3, 响应式, Proxy, ref, reactive, computed, watch, 组合式函数, Pinia, 性能] level: intermediate prerequisites: [frontend.typescript.type-system] est_hours: 3 related: [frontend.vue.scenario.ai-chat-stream, frontend.react.index, frontend.vue.interview] week: [7] order: 1 --- # Vue 3 响应式原理、组合式函数、Pinia 和性能优化 > 基于 Vue 3.5 编写,以官方文档为准:https://cn.vuejs.org/ ## 解释说明 ### 响应式原理(面试必问) - `reactive()` 用 **Proxy** 拦截对象的读写:读取时**收集依赖**(当前正在运行的副作用:组件渲染、`computed`、`watch`),写入时**触发**依赖重新执行。 - `ref()` 用对象的 `.value` getter / setter 实现,可以包装基本类型;模板中自动解包。 - `computed` 是带缓存的副作用:依赖不变就不重新计算。 - 更新是**异步批量**的:同一轮中多次修改只触发一次渲染,`await nextTick()` 之后才能拿到更新后的 DOM。 - Vue 2 用 `Object.defineProperty`,无法检测新增属性和数组索引赋值;Vue 3 的 Proxy 解决了这个问题。 | API | 用途 | 注意 | | --- | --- | --- | | `ref` / `reactive` | 响应式状态 | `reactive` 解构会丢失响应性,用 `toRefs`;整体替换 `reactive` 对象会断开引用 | | `shallowRef` | 只追踪 `.value` 的替换 | 大对象、第三方实例、只整体替换的数据,性能更好 | | `computed` | 派生状态 | 不要在里面做副作用 | | `watch` / `watchEffect` | 副作用(请求、存储) | 异步副作用要处理竞态(`onWatcherCleanup` / `onCleanup`) | | `effectScope` / `onScopeDispose` | 管理一组副作用的生命周期 | 组合式函数里清理定时器、请求、订阅 | ### 组合式函数(Composables) 把有状态的逻辑封装成 `useXxx()`:内部用 `ref`、`watch`、生命周期钩子,返回响应式状态和方法。和 React Hooks 的区别:`setup` 只执行一次,没有“依赖数组”和闭包陈旧值的问题,但要注意返回值的响应性和副作用清理。 ### Pinia 官方状态管理:`defineStore`,支持组合式写法;store 解构要用 `storeToRefs`;服务端渲染时每个请求要有独立的 Pinia 实例,避免跨请求状态污染。 ### 性能 `v-memo`、`v-once`;长列表虚拟滚动;大对象用 `shallowRef` / `markRaw`;组件懒加载 `defineAsyncComponent`;`computed` 代替模板中的复杂表达式;避免在模板中创建新对象 / 函数导致子组件重复渲染。 ## 案例 AI 对话页面:流式输出时每秒几十次追加文本,最初用 `reactive` 存整个消息数组并且每次替换,导致长对话卡顿。改为:已完成的消息用 `shallowRef` 数组(完成后整体替换一次),正在生成的消息单独用一个 `ref` 追加文本;Markdown 渲染做节流。把请求、解析、状态机封装成 `useChatStream` 组合式函数,组件卸载时自动停止生成。 ## 代码示例 组合式函数的核心(完整版本 [examples/ai-chat-stream/useChatStream.ts](examples/ai-chat-stream/useChatStream.ts),带 vitest 测试): ```ts export function useChatStream(opts: ChatOptions) { const status = ref("idle"); const text = ref(""); const final = shallowRef(null); // 整体替换的对象用 shallowRef let controller: AbortController | null = null; async function send(question: string) { controller?.abort(); // 新问题:取消上一次 controller = new AbortController(); status.value = "connecting"; const resp = await fetch(opts.url, { method: "POST", body: JSON.stringify({ question }), signal: controller.signal }); status.value = "streaming"; const feed = createSSEParser((e) => { if (e.event === "token") text.value += e.data; else if (e.event === "final") final.value = parseFinal(e.data); }); const reader = resp.body!.pipeThrough(new TextDecoderStream()).getReader(); for (;;) { const { value, done } = await reader.read(); if (done) break; feed(value); } status.value = "done"; } onScopeDispose(() => controller?.abort()); // 组件卸载时停止,避免泄漏 return { status: readonly(status), text: readonly(text), final, send, stop: () => controller?.abort() }; } ``` 在组件中使用: ```vue ``` ## 易错点 - 解构 `reactive` 对象或 Pinia store 后丢失响应性。 - `watch` 一个 `reactive` 对象的属性时写成 `watch(obj.count, ...)`:应该传 getter `() => obj.count`。 - 异步 `watch` 没有处理竞态:快速切换时旧请求的结果覆盖新结果。 - 在组合式函数里注册了定时器 / 订阅,没有在作用域销毁时清理。 - 用 `v-html` 渲染模型输出的 Markdown,没有做 XSS 清洗(见 [Web 安全专题](../../cs/web-security/index.md))。 - 把大量第三方对象(图表实例、编辑器)放进 `reactive`,造成深度代理的性能开销。 ## 面试常问 - Vue 3 的响应式原理?和 Vue 2 有什么区别? - `ref` 和 `reactive` 的区别? - `computed` 和 `watch` 的区别? - 组合式函数和 React Hooks 有什么区别?