Vue 组件渲染过程

Vue 组件从创建到渲染到页面的完整流程

问题

Vue 组件是如何从模板渲染成真实 DOM 的?

解答

Vue 组件渲染分为三个阶段:编译挂载更新

整体流程

模板 template
    ↓ 编译
render 函数
    ↓ 执行
VNode 虚拟 DOM
    ↓ patch
真实 DOM

1. 编译阶段

模板被编译成 render 函数:

// 模板
<template>
  <div class="container">
    <span>{{ message }}</span>
  </div>
</template>

// 编译后的 render 函数(Vue 3)
function render(_ctx) {
  return h('div', { class: 'container' }, [
    h('span', null, _ctx.message)
  ])
}

编译过程分三步:

// 1. parse:模板 → AST
const ast = parse(template)

// 2. transform:优化 AST,标记静态节点
transform(ast, options)

// 3. generate:AST → render 函数代码
const code = generate(ast)

2. 挂载阶段

执行 render 函数生成 VNode,再转换为真实 DOM:

// 简化的挂载流程
function mountComponent(vnode, container) {
  // 创建组件实例
  const instance = createComponentInstance(vnode)
  
  // 设置组件(处理 props、slots、setup 等)
  setupComponent(instance)
  
  // 设置渲染副作用
  setupRenderEffect(instance, container)
}

function setupRenderEffect(instance, container) {
  // 创建响应式副作用
  effect(() => {
    if (!instance.isMounted) {
      // 首次渲染
      const subTree = instance.render.call(instance.proxy)
      patch(null, subTree, container)
      instance.subTree = subTree
      instance.isMounted = true
    } else {
      // 更新渲染
      const nextTree = instance.render.call(instance.proxy)
      patch(instance.subTree, nextTree, container)
      instance.subTree = nextTree
    }
  })
}

3. Patch 过程

将 VNode 转换为真实 DOM:

function patch(n1, n2, container) {
  // n1: 旧 VNode,n2: 新 VNode
  
  if (n1 === null) {
    // 挂载
    mountElement(n2, container)
  } else {
    // 更新(diff)
    patchElement(n1, n2)
  }
}

function mountElement(vnode, container) {
  // 1. 创建 DOM 元素
  const el = document.createElement(vnode.type)
  vnode.el = el
  
  // 2. 处理 props
  if (vnode.props) {
    for (const key in vnode.props) {
      el.setAttribute(key, vnode.props[key])
    }
  }
  
  // 3. 处理子节点
  if (typeof vnode.children === 'string') {
    el.textContent = vnode.children
  } else if (Array.isArray(vnode.children)) {
    vnode.children.forEach(child => patch(null, child, el))
  }
  
  // 4. 插入容器
  container.appendChild(el)
}

4. 更新阶段

响应式数据变化时触发重新渲染:

// 响应式数据变化
const state = reactive({ count: 0 })

// 数据变化 → 触发 effect → 重新执行 render → patch 更新 DOM
state.count++

更新时的 diff 算法:

function patchChildren(n1, n2, container) {
  const oldChildren = n1.children
  const newChildren = n2.children
  
  // 双端 diff(Vue 3 使用快速 diff)
  let i = 0
  let e1 = oldChildren.length - 1
  let e2 = newChildren.length - 1
  
  // 从头比较
  while (i <= e1 && i <= e2) {
    if (isSameVNode(oldChildren[i], newChildren[i])) {
      patch(oldChildren[i], newChildren[i], container)
    } else {
      break
    }
    i++
  }
  
  // 从尾比较
  while (i <= e1 && i <= e2) {
    if (isSameVNode(oldChildren[e1], newChildren[e2])) {
      patch(oldChildren[e1], newChildren[e2], container)
    } else {
      break
    }
    e1--
    e2--
  }
  
  // 处理新增、删除、移动
  // ...
}

完整示例

// Vue 3 组件
const App = {
  setup() {
    const count = ref(0)
    const increment = () => count.value++
    return { count, increment }
  },
  // 这个 render 函数由模板编译生成
  render() {
    return h('div', [
      h('p', `Count: ${this.count}`),
      h('button', { onClick: this.increment }, 'Add')
    ])
  }
}

// 渲染流程:
// 1. createApp(App) - 创建应用实例
// 2. app.mount('#app') - 触发挂载
// 3. 执行 setup() - 初始化响应式数据
// 4. 执行 render() - 生成 VNode
// 5. patch() - 创建真实 DOM
// 6. 点击按钮 → count 变化 → 重新 render → patch 更新

关键点

  • 编译阶段:template → AST → render 函数,发生在构建时或运行时
  • VNode:render 函数返回虚拟 DOM 树,是真实 DOM 的 JS 描述
  • patch:对比新旧 VNode,最小化 DOM 操作
  • 响应式驱动:数据变化自动触发组件重新渲染
  • diff 算法:Vue 3 使用快速 diff,通过最长递增子序列优化移动操作