Vue 渲染过程

Vue 从模板到 DOM 的完整渲染流程

问题

Vue 组件从模板到最终渲染成 DOM 经历了哪些步骤?

解答

渲染流程概览

模板 template
    ↓ 编译
渲染函数 render function
    ↓ 执行
虚拟 DOM (VNode)
    ↓ patch
真实 DOM

1. 模板编译

Vue 将模板编译成渲染函数,分为三个阶段:

// 模板
const template = `<div id="app">{{ message }}</div>`

// 1. parse: 模板 -> AST
const ast = parse(template)
// 结果:{ tag: 'div', attrs: [{name: 'id', value: 'app'}], children: [...] }

// 2. optimize: 标记静态节点(优化 diff)
optimize(ast)

// 3. generate: AST -> 渲染函数代码
const code = generate(ast)
// 结果:'with(this){return _c("div",{attrs:{"id":"app"}},[_v(_s(message))])}'

2. 渲染函数执行

// Vue 内部的渲染函数辅助方法
// _c = createElement  创建元素 VNode
// _v = createTextVNode  创建文本 VNode
// _s = toString  转字符串

// 渲染函数执行后返回 VNode 树
const vnode = render.call(vm)

// VNode 结构
const vnode = {
  tag: 'div',
  data: { attrs: { id: 'app' } },
  children: [
    { text: 'Hello Vue' }
  ],
  elm: undefined  // 对应的真实 DOM,初始为空
}

3. Patch 过程(VNode -> DOM)

// 首次渲染:创建真实 DOM
function createElm(vnode) {
  const el = document.createElement(vnode.tag)
  
  // 设置属性
  if (vnode.data && vnode.data.attrs) {
    for (const key in vnode.data.attrs) {
      el.setAttribute(key, vnode.data.attrs[key])
    }
  }
  
  // 递归创建子节点
  if (vnode.children) {
    vnode.children.forEach(child => {
      if (child.text) {
        el.appendChild(document.createTextNode(child.text))
      } else {
        el.appendChild(createElm(child))
      }
    })
  }
  
  vnode.elm = el  // 保存真实 DOM 引用
  return el
}

4. 更新渲染(Diff + Patch)

// 数据变化时触发更新
vm.message = 'Updated'

// 1. 重新执行渲染函数,生成新 VNode
const newVnode = render.call(vm)

// 2. diff 比较新旧 VNode
function patch(oldVnode, newVnode) {
  // 同一节点,比较更新
  if (sameVnode(oldVnode, newVnode)) {
    patchVnode(oldVnode, newVnode)
  } else {
    // 不同节点,替换
    const parent = oldVnode.elm.parentNode
    const newElm = createElm(newVnode)
    parent.replaceChild(newElm, oldVnode.elm)
  }
}

// 判断是否同一节点
function sameVnode(a, b) {
  return a.key === b.key && a.tag === b.tag
}

// 更新节点
function patchVnode(oldVnode, newVnode) {
  const el = newVnode.elm = oldVnode.elm
  
  // 更新属性
  updateAttrs(el, oldVnode.data, newVnode.data)
  
  // 更新子节点(核心 diff 算法)
  updateChildren(el, oldVnode.children, newVnode.children)
}

5. 完整生命周期中的渲染

new Vue({
  template: '<div>{{ msg }}</div>',
  data: { msg: 'Hello' }
})

// 执行顺序:
// 1. beforeCreate
// 2. 初始化响应式数据
// 3. created
// 4. 编译模板(如果有 template)
// 5. beforeMount
// 6. 执行 render -> 生成 VNode -> patch 到 DOM
// 7. mounted(此时 $el 可用)

// 数据更新时:
// 1. beforeUpdate
// 2. 重新 render -> diff -> patch
// 3. updated

关键点

  • 编译阶段:template → AST → optimize → render function
  • 渲染阶段:执行 render 函数生成 VNode 树
  • 挂载阶段:patch 将 VNode 转为真实 DOM
  • 更新阶段:数据变化 → 重新 render → diff 比较 → 最小化 DOM 操作
  • 优化策略:静态节点标记跳过 diff,key 帮助复用节点