5.7 KiB
5.7 KiB
id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
| id | title | type | domain | stack | tags | level | prerequisites | est_hours | related | week | order | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| frontend.vue.reactivity-composition | Vue 3 响应式原理、组合式函数、Pinia 和性能优化 | lesson | frontend |
|
|
intermediate |
|
3 |
|
|
1 |
Vue 3 响应式原理、组合式函数、Pinia 和性能优化
基于 Vue 3.5 编写,以官方文档为准:https://cn.vuejs.org/
解释说明
响应式原理(面试必问)
reactive()用 Proxy 拦截对象的读写:读取时收集依赖(当前正在运行的副作用:组件渲染、computed、watch),写入时触发依赖重新执行。ref()用对象的.valuegetter / 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,带 vitest 测试):
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() };
}
在组件中使用:
<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 安全专题)。 - 把大量第三方对象(图表实例、编辑器)放进
reactive,造成深度代理的性能开销。
面试常问
- Vue 3 的响应式原理?和 Vue 2 有什么区别?
ref和reactive的区别?computed和watch的区别?- 组合式函数和 React Hooks 有什么区别?