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,通过最长递增子序列优化移动操作
目录