FumadocsZDecode
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 和 user

inject('theme') 在 Child 里通过原型链即可拿到,无需遍历整棵组件树,查找效率高。


六、对比总结

机制标记位置时机
keyAST 顶层 → VNode.data编译时提取,运行时 diff 使用
provide / inject不在 AST纯运行时,通过组件实例的 provides + 原型链实现

On this page