Vue
虚拟 DOM 与 AST
一、AST(抽象语法树)
作用:把模板字符串解析为结构化的 JS 对象,便于静态分析与后续转换。
Vue 编译流程:
template 字符串 → parse → AST → generate → render 函数示例:
<div id="app">{{ msg }}</div>经过 parse 后产生的 AST:
export const ast = {
type: 1, // 1 = 元素节点
tag: 'div',
attrsList: [{ name: 'id', value: 'app' }],
children: [
{
type: 2, // 2 = 带插值的文本
expression: '_s(msg)',
text: '{{ msg }}',
},
],
}generate 再把 AST 转成 render 函数字符串:
export default {
render() {
return _c('div', { attrs: { id: 'app' } }, [_v(_s(msg))])
},
}二、虚拟 DOM(VNode)
作用:用 JS 对象描述真实 DOM,避免频繁直接操作 DOM。
render 函数执行后产生 VNode:
export const vnode = {
tag: 'div',
data: { attrs: { id: 'app' } },
children: [{ tag: undefined, text: 'hello' }],
elm: null, // 对应的真实 DOM 引用
}核心流程:
数据变化 → 重新执行 render → 新 VNode
↓
diff(新旧 VNode 对比)
↓
patch(最小化更新真实 DOM)diff 关键点:
- 同层比较,不跨层,复杂度 O(n)
- 通过
key判断节点是否可复用 - Vue 2 使用双端比较;Vue 3 使用最长递增子序列算法
三、为什么需要这两层?
| 阶段 | 时机 | 目的 |
|---|---|---|
| AST | 编译时(一次) | 静态分析、优化(如标记静态节点) |
| VNode | 运行时(每次更新) | 高效 diff,减少真实 DOM 操作 |
Vue 3 在编译阶段就在 AST 上做静态提升和 PatchFlag 标记,运行时 diff 只比较动态部分,性能大幅提升。
四、列表中 key 的标记方式
key 在模板里是普通属性,但编译器会特殊对待它——不放进 attrs,而是提到 AST 节点顶层。
模板:
<li v-for="item in list" :key="item.id">{{ item.name }}</li>AST 节点(简化):
export const ast = {
type: 1,
tag: 'li',
for: 'list', // v-for 解析结果
alias: 'item',
key: 'item.id', // key 被单独提出,作为 AST 顶层属性
attrs: [], // key 不在这里
children: [],
}generate 后的 render 函数:
_l(list, function (item) {
return _c('li', { key: item.id }, [_v(_s(item.name))])
})
// ↑ key 在 VNode.data 顶层,不在 attrs 里为什么单独提出来? diff 算法需要在对比之前就拿到 key,用于判断节点能否复用。若埋在 attrs 里,每次都要遍历查找,性能差。
Vue 3 的编译产物类似,同时会附加 PatchFlag:
openBlock(true)
createBlock(
Fragment,
null,
renderList(list, (item) => {
return (
openBlock(),
createBlock('li', { key: item.id }, [_v(_s(item.name))])
)
}),
128 /* KEYED_FRAGMENT */,
)五、provide / inject 的标记方式
结论:provide / inject 不在 AST 里标记,它是纯运行时机制。
原因:AST 是单文件模板的产物,编译器只能看到当前组件,看不到完整的组件树,自然无法静态分析跨组件的依赖关系。
运行时实现(Vue 3)
每个组件实例上有两个关键字段:
instance = {
provides: {}, // 当前组件提供的数据
parent: parentInstance,
}provide 时:
function provide(key, value) {
const instance = getCurrentInstance()
let provides = instance.provides
const parentProvides = instance.parent?.provides
if (provides === parentProvides) {
// 第一次 provide:以父级 provides 为原型创建新对象
provides = instance.provides = Object.create(parentProvides)
}
provides[key] = value
}inject 时:
function inject(key) {
const instance = getCurrentInstance()
// 直接从父级的 provides 原型链上查找
const provides = instance.parent?.provides
if (provides && key in provides) {
return provides[key]
}
}核心机制:原型链查找
GrandParent.provides { theme: 'dark' }
↑ Object.create
Parent.provides { user: {...} } ← 原型链上能拿到 theme
↑ Object.create
Child.provides {} ← 原型链上能拿到 theme 和 userinject('theme') 在 Child 里通过原型链即可拿到,无需遍历整棵组件树,查找效率高。
六、对比总结
| 机制 | 标记位置 | 时机 |
|---|---|---|
key | AST 顶层 → VNode.data | 编译时提取,运行时 diff 使用 |
provide / inject | 不在 AST | 纯运行时,通过组件实例的 provides + 原型链实现 |