主题
实现渲染器 - mount 流程
本节对标 Vue 3 源码
@vue/runtime-core中的renderer.ts—— mount 部分
渲染器的职责
渲染器(Renderer)负责将 VNode 树转换为特定平台的真实 UI。核心流程:
VNode Tree → patch() → Real DOMpatch 是渲染器的核心函数,负责"打补丁" —— 将 VNode 渲染为真实节点,或者更新已有节点。
render 入口
ts
// 创建渲染器的工厂函数,接收平台相关的操作接口
// 这种设计将渲染逻辑与平台 API 解耦,是 Vue 3 跨平台渲染的基础
export function createRenderer(options: RendererOptions) {
// 解构出平台操作方法,后续在渲染过程中直接调用
const {
createElement, // 创建元素节点
setElementText, // 设置元素的文本内容
patchProp, // 设置/更新单个属性(class、style、事件等)
insert, // 将节点插入父容器
remove, // 从 DOM 中移除节点
createText, // 创建文本节点
setText, // 更新文本节点内容
} = options
// render 是渲染器的入口函数,外部通过它将 VNode 渲染到容器中
function render(vnode: VNode | null, container: any) {
if (vnode) {
// vnode 存在 → 调用 patch 进行挂载或更新
// n1 传 null 表示这是首次渲染(与 container._vnode 的更新在后续完成)
patch(null, vnode, container)
} else {
// vnode 为 null → 需要卸载之前渲染的内容
if (container._vnode) {
unmount(container._vnode)
}
}
// 将本次渲染的 vnode 缓存到容器上,下次渲染时作为旧 VNode 进行 Diff
container._vnode = vnode
}
// ... patch, mount, update 等内部函数
return { render } // 对外只暴露 render 方法
}vnode存在 → 执行 patch(挂载或更新)vnode为 null → 执行 unmount(卸载)container._vnode保存上一次渲染的 VNode,用于后续 Diff
patch 分发
ts
// patch 是渲染器的核心调度函数,负责根据 VNode 类型分发到对应的处理逻辑
function patch(
n1: VNode | null, // 旧 VNode(null 表示首次挂载)
n2: VNode, // 新 VNode
container: any, // 父容器节点
anchor: any = null, // 插入锚点,用于精确控制 DOM 插入位置
) {
// 如果旧 VNode 存在但新旧类型不同,则无法复用,先卸载旧节点
// 然后将 n1 置为 null,后续作为首次挂载处理
if (n1 && !isSameVNodeType(n1, n2)) {
unmount(n1)
n1 = null
}
const { type, shapeFlag } = n2
// 根据 VNode 的类型(type 或 shapeFlag)分发到不同的处理函数
switch (type) {
case Text:
// 文本节点走专门的文本处理逻辑
processText(n1, n2, container)
break
case Fragment:
// Fragment 没有真实容器,直接处理子节点
processFragment(n1, n2, container)
break
default:
// 默认分支:通过 shapeFlag 位运算判断具体类型
if (shapeFlag & ShapeFlags.ELEMENT) {
// 普通 DOM 元素
processElement(n1, n2, container, anchor)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 组件(有状态组件或函数式组件)
processComponent(n1, n2, container, anchor)
}
}
}patch 的逻辑
- 如果新旧 VNode 类型不同 → 卸载旧的,当作首次挂载
- 根据 VNode 类型分发到不同的 process 函数
n1 === null表示首次挂载(mount),否则是更新(update)
mountElement —— 挂载 DOM 元素
ts
// processElement 根据是否有旧节点来决定挂载还是更新
function processElement(
n1: VNode | null, // 旧 VNode
n2: VNode, // 新 VNode
container: any, // 父容器
anchor: any, // 插入锚点
) {
if (!n1) {
// n1 为 null → 首次挂载,走 mountElement 逻辑
mountElement(n2, container, anchor)
} else {
// n1 存在 → 更新已有元素,走 patchElement 逻辑
patchElement(n1, n2)
}
}
// mountElement 完成元素节点的首次挂载,分四步进行
function mountElement(vnode: VNode, container: any, anchor: any) {
// 第 1 步:调用平台 API 创建真实 DOM 元素,并将引用保存到 vnode.el
// 这样后续更新时可以直接通过 vnode.el 访问真实 DOM
const el = (vnode.el = createElement(vnode.type as string))
// 第 2 步:处理子节点
const { children, shapeFlag } = vnode
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 子节点是纯文本 → 直接设置元素的文本内容
setElementText(el, children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 子节点是 VNode 数组 → 递归挂载每个子节点
mountChildren(children as VNode[], el)
}
// 第 3 步:遍历 props 设置元素属性(class、style、事件监听器等)
// prevValue 传 null 因为是首次挂载,不存在旧值
if (vnode.props) {
for (const key in vnode.props) {
patchProp(el, key, null, vnode.props[key])
}
}
// 第 4 步:将创建好的元素插入到父容器中
// anchor 用于指定插入位置,为 null 时默认追加到末尾
insert(el, container, anchor)
}
// 递归挂载子节点数组,对每个子 VNode 调用 patch
function mountChildren(children: VNode[], container: any) {
children.forEach((child) => {
// n1 传 null 表示每个子节点都是首次挂载
patch(null, child, container)
})
}mount 的四步流程
1. createElement('div') → 创建 <div> 元素
2. 处理 children → 递归 patch 子节点
3. patchProp(el, key, null, v) → 设置 class/style/事件等
4. insert(el, container) → 插入到父容器注意 vnode.el = el —— 将真实 DOM 节点的引用保存到 VNode 上,后续更新时直接操作它。
processText —— 文本节点
ts
// processText 处理文本类型的 VNode
function processText(
n1: VNode | null,
n2: VNode,
container: any,
) {
if (!n1) {
// 首次挂载:创建文本节点并插入容器
const el = (n2.el = createText(n2.children as string))
insert(el, container)
} else {
// 更新:复用旧的真实 DOM 节点(将 n1.el 赋给 n2.el)
const el = (n2.el = n1.el!)
// 只在文本内容发生变化时才更新,避免不必要的 DOM 操作
if (n2.children !== n1.children) {
setText(el, n2.children as string)
}
}
}processFragment —— Fragment
Fragment 没有真实的 DOM 容器,直接挂载子节点:
ts
// processFragment 处理 Fragment 类型的 VNode
// Fragment 本身不产生真实 DOM 节点,只是作为子节点的逻辑容器
function processFragment(
n1: VNode | null,
n2: VNode,
container: any,
) {
if (!n1) {
// 首次挂载:直接将子节点挂载到容器中(不会创建额外的包裹元素)
mountChildren(n2.children as VNode[], container)
} else {
// 更新:对子节点进行 Diff 更新
patchChildren(n1, n2, container)
}
}processComponent —— 组件(初步)
组件的 mount 流程将在第 13 节详细实现,这里先给出框架:
ts
// processComponent 处理组件类型的 VNode
// 组件也遵循 "首次挂载 vs 更新" 的分支逻辑
function processComponent(
n1: VNode | null,
n2: VNode,
container: any,
anchor: any,
) {
if (!n1) {
// 首次挂载组件:创建组件实例、执行 setup、渲染子树等
mountComponent(n2, container, anchor)
} else {
// 组件更新:对比新旧 props,决定是否需要重新渲染
updateComponent(n1, n2)
}
}unmount —— 卸载
ts
// unmount 卸载 VNode,将对应的真实 DOM 从文档中移除
function unmount(vnode: VNode) {
const { type, children, shapeFlag } = vnode
if (type === Fragment) {
// Fragment 没有自身的 DOM 节点,需要递归卸载其所有子节点
;(children as VNode[]).forEach((child) => unmount(child))
return
}
if (shapeFlag & ShapeFlags.COMPONENT) {
// 组件类型:调用组件的卸载逻辑(清理副作用、生命周期钩子等)
unmountComponent(vnode.component)
return
}
// 普通元素:直接调用平台 API 移除真实 DOM 节点
remove(vnode.el!)
}RendererOptions —— 平台抽象
ts
// RendererOptions 定义了渲染器所需的平台操作接口
// 不同平台(DOM、Canvas、Native 等)只需实现这些方法即可
interface RendererOptions {
createElement(type: string): any // 创建元素节点
setElementText(el: any, text: string): void // 设置元素的文本内容
insert(el: any, parent: any, anchor?: any): void // 将节点插入到父节点中指定位置
remove(el: any): void // 从父节点移除指定节点
createText(text: string): any // 创建文本节点
setText(node: any, text: string): void // 更新文本节点内容
patchProp(el: any, key: string, prevValue: any, nextValue: any): void // 设置/更新节点属性
}这是 Vue 3 跨平台渲染的关键设计 —— 渲染器不直接调用 DOM API,而是通过 options 注入。DOM 平台的实现由 runtime-dom 提供。
对比 React
| 维度 | Vue 3 Renderer | React Reconciler |
|---|---|---|
| 入口 | render() → patch() | render() → reconcile() |
| 节点类型分发 | switch (type) + shapeFlag | switch (fiber.tag) |
| 挂载 | mountElement() | completeWork() |
| 平台抽象 | RendererOptions | HostConfig |
| 真实节点引用 | vnode.el | fiber.stateNode |
测试用例
ts
describe('renderer: mount', () => {
// 测试基本元素挂载:带属性和文本子节点的 div
it('should mount element', () => {
const vnode = h('div', { id: 'foo' }, 'hello')
const root = document.createElement('div')
render(vnode, root) // 将 VNode 渲染到 root 容器
// 验证生成的 HTML 结构是否正确,id 属性和文本内容都应存在
expect(root.innerHTML).toBe('<div id="foo">hello</div>')
})
// 测试嵌套元素挂载:验证子节点递归渲染是否正确
it('should mount nested elements', () => {
const vnode = h('div', null, [
h('p', null, 'paragraph'),
h('span', null, 'text'),
])
const root = document.createElement('div')
render(vnode, root)
// 验证嵌套结构:div 内包含 p 和 span 两个子元素
expect(root.innerHTML).toBe('<div><p>paragraph</p><span>text</span></div>')
})
// 测试文本节点挂载:使用 Text 类型创建纯文本 VNode
it('should mount text node', () => {
const vnode = createVNode(Text, null, 'hello text')
const root = document.createElement('div')
render(vnode, root)
// 文本节点不会产生 HTML 标签,只有纯文本内容
expect(root.textContent).toBe('hello text')
})
})本节小结
- render → patch → process → mount 四层调用链
- patch 分发 — 根据 VNode 类型分派到不同的处理函数
- mountElement 四步 — 创建元素、处理子节点、设置属性、插入容器
- RendererOptions — 平台抽象,实现跨平台渲染能力
下一节实现渲染器的 update 流程。