Vue:运行时依赖追踪与编译优化协作
最后核验:2026-06-30|适用版本:Vue 2.7、3.4–3.5(示例锁定 3.5.39)|前置知识:JavaScript、TypeScript、Browser / Web APIs
Vue 的渐进采用边界与固定成本
Vue 把两类机制组合在一起:运行时响应式系统追踪哪些 effect 读取了哪些值,模板编译器标记哪些节点和属性可能变化。前者让状态组合直观,后者减少 Virtual DOM 更新时要检查的范围。理解这条链,才能解释 reactive 解构为何失去连接、computed 与 watch 为什么不是同一种工具、模板为何比随意手写 render function 更容易优化。
Vue 适合渐进增强、单页应用和需要清晰模板/逻辑组织的团队;同一核心可以从 CDN 小组件扩展到构建型应用。若需求是静态内容加一个原生表单,Vue 仍是额外运行时;若需求已经确定为文件路由、SSR、服务端 API 和混合渲染,应直接评估 Nuxt,而不是在客户端 Vue 上重造应用框架。Ways of Using Vue
不要因模板“像 HTML”就忽略平台语义。v-if、v-for 和组件最终仍生成 DOM;合法内容模型、焦点、表格语义与浏览器性能不会被 Vue 自动保证。
单文件组件、模板与组件契约
Single-File Component(SFC)把 <script>、<template>、<style> 放在 .vue 文件中,由 @vue/compiler-sfc 拆分并交给 script/template/style 编译器。<script setup> 是编译语法:顶层绑定自动暴露给模板,defineProps、defineEmits、defineModel 等宏在编译时处理,不是运行时全局函数。
<script setup lang="ts">
type Entry = { id: string; name: string };
const props = defineProps<{
entry: Entry;
selected?: boolean;
}>();
const emit = defineEmits<{
toggle: [id: string];
}>();
</script>
<template>
<article>
<h3>{{ props.entry.name }}</h3>
<button type="button"
:aria-pressed="props.selected ?? false"
@click="emit('toggle', props.entry.id)">
对比
</button>
</article>
</template>props 是父到子的只读输入,子组件不直接修改;emit 是子向父报告事件,不产生浏览器 DOM 冒泡。父组件需要监听祖孙事件时应逐层明确转发或提升状态,不使用隐式事件总线。attribute fallthrough 会把未声明属性落到单根节点;多根组件需用 $attrs 明确放置,避免 class/aria/事件丢失。
slot 让父级提供内容,slot 内容在父级作用域求值;scoped slot 由子级提供 slot props。只需固定标题/正文时直接 props/普通 slot,不为每个文本创建动态组件。
v-model 是 prop + update event 约定。Vue 3.4+ 的 defineModel() 可声明模型,但双向绑定意味着父子共同协议,不用于任意对象深层共享。表单控件仍遵循原生 value/checked/selected 和输入法合成事件语义。
从 SFC 到 Render Function 与 DOM
构建时,SFC parser 读取 descriptor;script 编译处理 setup/宏/TypeScript;template 编译经历 parse、transform、codegen,输出 render function;style 编译处理 scoped/module/preprocessor。生产运行时通常不带模板编译器,浏览器执行预编译 render function。
模板表达式运行在组件 render 上下文,只做同步表达式,不写声明/流程语句。v-if 改变子树是否存在并触发生命周期;v-show 保留节点只切 display,适合频繁切换;v-for 需要稳定 key。v-if 与 v-for 不放同一元素猜优先级,先用 computed 过滤或外层 template。
scoped CSS 通过编译器为元素和选择器附加 scope attribute,不是 Shadow DOM:全局样式、继承、自定义属性和脚本仍能跨边界。深度选择器会增加父组件对后代实现的耦合,只用于第三方/子组件明确样式接口。
Proxy、ref 与依赖追踪
Vue 3 对对象使用 Proxy,对 ref 使用 getter/setter。effect 执行期间读取响应式属性时,track(target, key) 把当前 effect 放进订阅集合;写入触发 trigger(target, key),相关 effect 被调度。官方用 WeakMap<target, Map<key, Set<effect>>> 解释核心关系。Reactivity in Depth
组件渲染本身是 Reactive Effect(响应式副作用)。首次渲染读取 query、entries 和 selectedIds,这些依赖变化后组件更新被排入队列;同一 tick 的多次同步写入会批量处理。需要等待 DOM 更新完成时使用 nextTick(),不要用固定 setTimeout 猜提交时机。
ref 适合可替换的任意值;模板会自动解包顶层 ref,脚本中仍显式 .value。reactive 深度代理对象且返回值与原对象身份不同,不能用另一个普通对象整体替换原代理引用。把响应式属性解构成普通局部变量会绕过 Proxy 访问;使用 toRef、toRefs,或保持对象访问。Reactivity Fundamentals
| API | 读取与失效 | 适用问题 | 常见误用 |
|---|---|---|---|
ref | .value getter/setter | 标量、可替换对象、模板状态 | 在模板中到处手写 .value |
reactive | Proxy 属性 | 聚合且原地更新的对象 | 解构后期待原始值仍响应 |
computed | 惰性缓存纯派生 | 搜索结果、总计、格式化 | 发请求或修改其他状态 |
watch | 显式 source | 某值变化后同步外部系统 | 复制可以 computed 的状态 |
watchEffect | 执行时自动收集 | 依赖与副作用高度一致 | 依赖隐蔽、异步后读取不被追踪 |
Composition API 按业务能力组织状态、派生和副作用;Options API 则按 data、computed、methods 等选项组织。Vue 3 的 Options API 建立在 Composition API 之上,不是旧运行时。小组件两者都清楚时无需强制迁移;跨组件复用带状态逻辑时,composable 比 mixin 的隐式成员来源更可追踪。
computed、watch 与调度队列
以下单文件组件展示 ref、computed 和只同步 URL 的 watch。环境是 Vue 3.5、TypeScript 6、Vite 8。
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
type Entry = { id: string; name: string; tags: string[] };
const props = defineProps<{ entries: Entry[] }>();
const query = ref('');
const selectedIds = ref<string[]>([]);
const filtered = computed(() => {
const term = query.value.trim().toLowerCase();
return props.entries.filter(({ name, tags }) =>
[name, ...tags].some((value) => value.toLowerCase().includes(term)));
});
watch([query, selectedIds], () => {
const params = new URLSearchParams();
if (query.value) params.set('q', query.value);
if (selectedIds.value.length) params.set('compare', selectedIds.value.join(','));
history.replaceState(null, '', `?${params}`);
}, { deep: true });
function toggle(id: string) {
selectedIds.value = selectedIds.value.includes(id)
? selectedIds.value.filter((item) => item !== id)
: selectedIds.value.length < 4 ? [...selectedIds.value, id] : selectedIds.value;
}
</script>
<template>
<label for="q">搜索</label>
<input id="q" v-model="query" type="search">
<ul>
<li v-for="entry in filtered" :key="entry.id">
{{ entry.name }}
<button type="button" :aria-pressed="selectedIds.includes(entry.id)"
@click="toggle(entry.id)">对比</button>
</li>
</ul>
</template>运行本仓库示例:
pnpm --filter @roadmap/vue-slice test
pnpm --filter @roadmap/vue-slice preview --port 4202输入:访问 http://127.0.0.1:4202/,搜索 proxy,选择四项后尝试第五项,再刷新。
预期输出:2 项测试、vue-tsc 与生产构建通过;搜索匹配 Vue,选择保持四项,URL 写入并恢复查询与选择。八项数据无需防抖、虚拟列表或异步 Store。
computed getter 应保持纯。它的缓存按响应式依赖失效,而不是按函数参数;返回新对象会让引用每次变化,可能继续触发下游 effect。Vue 3.4+ 对 computed 结果稳定性做了优化,但复杂对象仍应先减少不必要派生,而不是手写深比较。Vue Performance
watch 的 source 决定依赖:直接传 state.count 只传入当前 number,应写 getter () => state.count;reactive 对象作为 source 默认深层追踪,深遍历大图成本高。flush: 'pre' 默认在组件 DOM 更新前执行,post 在更新后,sync 每次写入同步触发且不批处理,只用于极小同步需求。异步 watcher 在第一个 await 后读取的值不会自动成为 watchEffect 依赖。
Vue 3.5 的 onWatcherCleanup 必须在 watcher 同步执行阶段注册;传统 onCleanup 参数也可绑定当前 watcher。请求 watcher 应取消陈旧结果:
watch(query, async (value, _oldValue, onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort());
entries.value = await loadEntries(value, controller.signal);
}, { flush: 'post' });VNode、Block Tree 与 keyed Diff
Render function 返回 VNode。首次挂载递归创建 DOM;更新时 patch(oldVNode, newVNode) 先比较 type/key:不同则卸载替换,相同则更新 props/children。文本、数组、组件、Fragment、Teleport、Suspense 各有处理分支。
模板编译器把稳定静态节点提升到 render 外,给动态 VNode 标注 Patch Flags(如 TEXT、CLASS、PROPS),并把动态后代收集进 Block Tree。运行时因此可以跳过静态子树,只访问编译器已知的动态节点;随意手写 render function 可能失去这些提示。
keyed children diff 会同步处理头尾相同节点,再为未知区间建立 key → new index 映射,卸载消失项、patch 存在项,并用最长递增子序列减少 DOM 移动。这个算法不是任意树最优 diff;稳定 key 才能表达业务身份。
<li v-for="entry in entries" :key="entry.id">
<input v-model="notes[entry.id]" :aria-label="`${entry.name} 备注`">
</li>使用 index key 或缺 key 时,插入/排序会按位置复用 DOM/组件实例,输入值、焦点、transition 和本地 state 可能对应错条目。key 只需兄弟中唯一,不能每次 render 用随机数,否则所有节点被强制重建。
组件更新由渲染 effect 调度。同一 tick 多次状态写入进入去重队列;父组件通常先于子组件更新,pre/post flush callback 在相应阶段运行。nextTick 等待当前批次 DOM flush,不保证图片加载、CSS transition 或浏览器绘制完成。
SSR、水合、Teleport 与异步边界
SSR 为每个请求创建 app,把 VNode 渲染为 HTML;浏览器解析后,hydrate 复用现有 DOM 并关联组件/事件。服务端与客户端首次 render 必须使用一致数据、时区/locale 和随机结果。非法 HTML 被浏览器纠错也会造成结构 mismatch。
Vue 3.5 支持 hydration strategies 让异步组件在 idle、visible、media 或 interaction 时水合;延迟只适合不需立即交互的区域,且服务端 HTML 要可读。useId 为 SSR/客户端生成稳定 id。data-allow-mismatch 只声明已理解的特定差异,不修复权限、内容或结构错误。
Teleport 把子内容渲染到组件树外的 DOM 目标,但逻辑父子、provide/inject 和事件仍按组件树;目标必须在挂载时存在,SSR 还需框架正确注入 teleport 内容。Suspense 协调 async setup/异步组件的 pending/fallback,目前使用前应核对稳定性和 SSR 行为,不把它当通用错误边界。
Composition API 中的状态与清理边界
Vue 示例用 ref 保存目录、查询、选择、详情和通知;computed 生成过滤结果与已选条目;watch 只把查询和选择同步到 History。数据加载在 onMounted 中执行,onUnmounted 取消 Fetch。
详情使用原生 dialog。watch(active) 等待 nextTick() 后调用 showModal(),因为这是与 DOM 小部件的外部协调;关闭事件清空详情并恢复触发按钮焦点。模板保留真实 label、button、table 和 caption,不用组件封装隐藏语义。
| 示例能力 | Vue 原语 | 选择原因 |
|---|---|---|
| 异步目录 | ref<Entry[]> | 成功后整体替换一次 |
| 搜索结果 | computed | 纯派生且依赖明确 |
| URL 同步 | watch | History 是外部系统 |
| 详情 DOM | template ref + nextTick | Commit 后调用原生方法 |
| 请求取消 | 生命周期 + AbortController | 防止卸载后陈旧更新 |
| 本地对比 | ref<string[]> | 小型可序列化状态 |
这里不使用 Pinia。一个页面只有一个状态拥有者,composable 也没有第二个调用点;提前提取 Store 只会增加间接层。出现跨路由共享、开发工具时间线或独立领域所有权后,再评估 Pinia。
Vue Devtools、编译优化与原始 HTML 风险
Vue DevTools 查看组件、props、响应式状态和性能;开发环境的 onRenderTracked、onRenderTriggered 以及 computed/watch 的 onTrack、onTrigger 能定位具体依赖。生产性能仍要用 Chrome Performance 与真实产物测量,开启 app.config.performance 可加入 Vue 标记。Reactivity Debugging
Vue 模板先编译成 render function。编译器做静态提升、缓存事件处理器并生成 Patch Flags,运行时只遍历动态节点形成的 Block Tree;模板因此提供比任意手写 VNode 更强的优化信息。Rendering Mechanism
性能先区分加载与更新:内容敏感页面不应默认纯 SPA;生产构建预编译模板,不向浏览器发送编译器;路由组件按需分割;大列表经测量后虚拟化;访问十万级深层属性出现成本时,才用 shallowRef 并把内部数据当不可变。官方同样提醒避免无意义组件抽象,因为每个组件实例都有成本。Performance Guide
安全上,文本插值和普通绑定默认转义,但 v-html 直接写入 HTML,不能接收用户内容。URL、样式和服务端模板仍需按上下文验证;前端路由守卫不能替代服务端授权。SSR 必须为每个请求创建独立应用和状态,防止跨请求状态污染。Security、SSR
常见误区:解构 reactive 后期待标量响应;用 watch 保存过滤结果;在 computed 中修改依赖;滥用 deep watch 扫描大对象;列表缺稳定 key;把 nextTick 当任意延迟;用 v-if 和 v-for 在同一元素制造不清晰优先级;用 v-html 渲染不可信 Markdown。
重大重构、关键 bug 与 Vue 2.7 到 3.5 迁移
| 节点 | 重大重构 | 行为风险 | 升级证据 |
|---|---|---|---|
| Vue 2 → 3 | defineProperty 改为 Proxy,Composition API、Fragments、Teleport、新编译器与应用实例 | 全局 API、过滤器、事件 API、数组/属性观察语义和第三方插件变化 | migration build 告警、组件/插件清单、SSR 水合 |
| Vue 3.4 | 模板 parser 重写并提升解析速度;响应式触发重构,defineModel 稳定 | 依赖旧 parser 容错或 effect 重入顺序的代码需复测 | SFC 编译快照、effect/watch 调度测试 |
| Vue 3.5 | 响应式内部再次重构,降低内存;稳定响应式 props 解构,加入懒水合、useId、onWatcherCleanup | 解构依赖、清理时机和 SSR 首屏输入变化 | heap snapshot、watch cleanup、SSR/CSR HTML 对照 |
Vue 3.5.6 出现响应式依赖清理相关内存泄漏。官方 3.5.7 changelog 修复了开发态 subsHead 未更新,以及垃圾回收对象对应 dep 未从依赖表释放的问题(#11956、#11971、#11979)。这类严重 bug 的症状是组件已经卸载但 heap 持续增长,不能靠手工把业务变量置空修复;应升级至少 3.5.7,并用重复挂载/卸载场景比较 heap snapshot 与 retaining path。Vue 3.5.7 changelog、问题 #11956
补丁版本也必须保持 vue、@vue/compiler-sfc、@vue/server-renderer 同线,避免编译提示与运行时 patch flag 不一致。升级时先跑响应式单测和生产 SSR,再检查 hydration mismatch、KeepAlive/Transition、异步组件和卸载后的订阅数量。
Vue 2 使用 Object.defineProperty getter/setter,新增/删除属性和数组索引存在限制;Vue 3 改用 Proxy、Composition API 和新的编译/运行时架构。Vue 2 已结束生命周期,2.7 只适合作为迁移桥梁;新项目不应基于兼容构建长期停留。Vue 2 EOL
Vue 3.5 是无破坏性 minor,重构响应式系统以改善内存与深层数组追踪,并稳定响应式 props 解构;SSR 增加懒水合策略、useId 和选择性水合差异标记,还加入 useTemplateRef 与 onWatcherCleanup。Announcing Vue 3.5
截至核验日,3.5 是稳定线,3.6 仍处于 beta,因此只在未来方向讨论。Vapor Mode 探索利用既有响应式图生成不依赖 Virtual DOM 的更新代码,它代表潜在编译策略,不应作为当前生产契约。稳定文档仍把 Vue 描述为以运行时响应式为主、由编译器辅助优化。Vue Core Releases
升级顺序:先升级 3.5 最新 patch;处理编译器、runtime-dom 与服务端渲染器的同版本要求;运行类型、SSR 和水合测试;最后评估响应式 props 解构或懒水合是否减少现有代码。不要把 beta 行为写进稳定数据层。
Vue、React 与 Nuxt 的职责差异
| 约束 | Vue | React | Angular | Svelte |
|---|---|---|---|---|
| 更新依赖 | Proxy/ref 自动追踪 | 状态触发组件 Render | Signals + 框架调度 | 编译器生成细粒度更新 |
| 模板 | HTML-based SFC | JSX | Angular template | Svelte template |
| 派生状态 | computed | Render / useMemo | computed Signal | $derived |
| 外部同步 | watch / lifecycle | Effect | effect / RxJS | $effect |
| 渐进增强 | 可 CDN 局部挂载 | 通常构建型根节点 | 更偏完整应用 | 需要编译步骤 |
| 完整 Meta-framework | Nuxt | Next.js 等 | Angular 自带较多工程能力 | SvelteKit |
Vue 的平衡点是运行时依赖追踪、模板编译和渐进采用。若团队希望显式单向 Render 与庞大 React 生态,React 更合适;若需要强约束 DI、表单、Router 和企业级一体化,Angular 更直接;若希望编译期细粒度语法和更少运行时代码,可比较 Svelte。
响应式追踪与组件验收练习
- 实现一个
reactive对象并解构属性,解释哪个绑定失去响应以及为何。 - 用
computed替换保存筛选结果的watch,确认删除一份状态。 - 在
onRenderTracked中定位查询输入触发的依赖,再移除调试钩子。 - 快速切换两次查询请求,用 watcher cleanup 或生命周期取消旧请求。
- 比较模板编译输出的 Patch Flags 与手写 render function。
- 在大只读数组上比较
ref与shallowRef,没有测量差异时保留ref。 - 服务端渲染后故意制造时间差异,定位水合警告而非直接压制。
- 用键盘完成搜索、详情和四项对比,验证焦点与状态通知。
验收标准:能画出 track/trigger 关系,区分 computed 与 watch,解释编译器如何缩小 Patch 范围,并能根据路由和服务端约束判断 Vue 还是 Nuxt。只会背 ref 与 reactive 语法不算通过。
Vue 官方文档、RFC 与 Release
- Vue Guide:Vue 3 当前稳定概念与使用方式;核验于 2026-06-30。
- Reactivity in Depth:Proxy、track/trigger、effect 与 Signals 对比;核验于 2026-06-30。
- Rendering Mechanism:模板编译、Virtual DOM、Patch Flags 与 Block Tree;核验于 2026-06-30。
- Composition API FAQ:与 Options API、mixins 和类型推断的取舍;核验于 2026-06-30。
- Announcing Vue 3.5:响应式重构、SSR 和 watcher 清理;核验于 2026-06-30。
- Vue Core Releases:3.5 patch 与 3.6 beta 状态;核验于 2026-06-30。