Files
zhuangang/topics/frontend/vue/01-reactivity-composition.md

120 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<ChatStatus>("idle");
const text = ref("");
const final = shallowRef<FinalPayload | null>(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
<script setup lang="ts">
import { useChatStream } from "./useChatStream";
const { status, text, final, send, stop } = useChatStream({ url: "/api/chat" });
</script>
<template>
<p v-if="status === 'connecting'">连接中…</p>
<article>{{ text }}</article>
<ul v-if="final"><li v-for="c in final.citations" :key="c.id">{{ c.title }}</li></ul>
<button v-if="status === 'streaming'" @click="stop">停止生成</button>
</template>
```
## 易错点
- 解构 `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 有什么区别?