phase 5 part 1: typescript, vue, react, ssr(Next.js 16) topics with lessons, scenarios, interviews, verified examples
This commit is contained in:
1 parent
7a32d2c9a6
commit
7b5cae7d24
39 files changed
+2482
-48
No files matched your search
@@ -0,0 +1,119 @@
|
||||
---
|
||||
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 有什么区别?
|
||||
Reference in new issue
Block a user