2026.01.08
Renderer -> 挂载与卸载机制
在之前的学习过程中,我们一直使用 Vue 官方提供的 createRenderer 和 render 函数。现在,让我们深入了解其内部实现原理,创建属于自己的渲染器。
在之前的学习过程中,我们一直使用 Vue 官方提供的 createRenderer 和 render 函数。现在,让我们深入了解其内部实现原理,创建属于自己的渲染器。
基础用法
首先,让我们回顾一下如何使用 createRenderer 创建一个自定义渲染器:
import { createRenderer, h } from 'vue'
import { renderOptions } from '../dist/vue.esm.js'
// 创建渲染器实例
const renderer = createRenderer(renderOptions)
// 创建虚拟节点
const vnode = h('div', 'hello world')
// 渲染到目标容器
renderer.render(vnode, app)
渲染器的核心结构
createRenderer 函数会返回一个包含 render 方法的对象。让我们看看基础的实现结构:
export function createRenderer(options) {
console.log(options)
const render = (vnode, container) => {
console.log(vnode, container)
}
return {
render
}
}
渲染器的核心流程
渲染器需要处理三个关键场景:
- 挂载(Mount) - 初次渲染虚拟节点到真实 DOM
- 更新(Update) - 对比新旧节点,更新已存在的 DOM
- 卸载(Unmount) - 移除不再需要的 DOM 节点
patch 函数的设计
patch 函数是处理挂载和更新的核心函数:
export function createRenderer(options) {
/**
* 更新和挂载所要用的函数
* @param n1 老节点。如果为 null,则挂载 n2;如果存在,则与 n2 做 diff
* @param n2 新节点
* @param container 要挂载的容器
*/
function patch(n1, n2, container) {}
function render(vnode, container) {
// 1. 挂载:首次渲染,没有老节点
patch(null, vnode, container)
// 2. 更新:保存当前 vnode 供下次对比
container._vnode = vnode
// 3. 卸载:只有已渲染的内容才需要卸载
unmount(container._vnode)
}
return {
render
}
}
render 函数的完整实现
现在让我们看看 render 函数的完整逻辑:
export function createRenderer(options) {
function patch(n1, n2, container) {
// 如果新旧节点相同,无需更新
if (n1 === n2)
return
// 如果老节点不存在,说明是首次挂载
if (n1 === null) {
mountElement(n2, container)
}
}
function unmount(vnode) {
// 卸载函数的实现稍后完成
}
function render(vnode, container) {
if (vnode === null) {
// 如果新的 vnode 为 null,且容器中有老节点,则卸载
if (container._vnode) {
unmount(container._vnode)
}
}
else {
// 将老节点和新节点进行 patch
// 如果 container._vnode 不存在(首次渲染),则传 null
patch(container._vnode || null, vnode, container)
}
// 更新容器的 _vnode 属性,供下次渲染使用
container._vnode = vnode
}
return {
render
}
}
mountElement 实现元素挂载
mountElement 函数负责将虚拟节点挂载到真实 DOM:
export function createRenderer(options) {
const {
createElement: hostCreateElement,
insert: hostInsert,
remove: hostRemove,
setElementText: hostSetElementText,
createText: hostCreateText,
setText: hostSetText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
patchProp: hostPatchProp
} = options
function mountElement(vnode, container) {
const { type, props, children, shapeFlag } = vnode
// 1. 创建 DOM 元素
const el = hostCreateElement(type)
// 2. 挂载 props(属性和事件)
if (props) {
for (const key in props) {
hostPatchProp(el, key, null, props[key])
}
}
// 3. 处理子节点
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 如果子节点是文本
hostSetElementText(el, children)
}
else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 如果子节点是数组
mountChildren(children, el)
}
// 4. 将元素插入到容器中
hostInsert(el, container)
}
// ...
}
mountChildren 挂载子节点
mountChildren 函数用于挂载数组类型的子节点:
function mountChildren(children, container) {
// 因为之前的挂载和更新已经有 patch 了,所以复用即可
for (let i = 0; i < children.length; i++) {
const child = children[i]
// 递归调用 patch,首次挂载传入 null 作为老节点
patch(null, child, container)
}
}
通过递归调用 patch 函数,我们可以处理嵌套的子节点结构。
unmount 实现节点卸载
unmount 函数用于卸载虚拟节点对应的真实 DOM:
function unmount(vnode) {
/**
* 如何卸载呢?
* - 知道挂载到哪就可以卸载了
* - 之前 vnode 上有个 el 属性,一直是 null
* - 在挂载的时候更新一下不就可以卸载了嘛
*/
hostRemove(vnode.el)
}
function mountElement(vnode, container) {
const { type, props, children, shapeFlag } = vnode
// 创建 DOM 元素
const el = hostCreateElement(type)
// 关键:将真实 DOM 元素保存到 vnode.el 上
// 这样在卸载时就能找到对应的真实 DOM 进行删除
vnode.el = el
// ...后续的 props 挂载和子节点处理
}
通过在 mountElement 中保存 vnode.el 的引用,我们就能在 unmount 时找到对应的真实 DOM 并将其移除。
总结
通过本章的学习,我们了解了:
- 渲染器的三大核心功能:
- 挂载(Mount):将虚拟节点转换为真实 DOM
- 更新(Update):对比新旧节点进行差异更新
- 卸载(Unmount):移除不再需要的 DOM 节点
- patch 函数的作用:
- 作为统一的入口处理挂载和更新
- 通过判断老节点是否存在来决定是挂载还是更新
- 使用递归处理子节点
- mountElement 的实现步骤:
- 创建 DOM 元素
- 处理 props(属性和事件)
- 根据 shapeFlag 判断子节点类型并处理
- 插入到容器中
- 关键设计细节:
- 使用
container._vnode保存上一次的虚拟节点 - 使用
vnode.el保存对应的真实 DOM 元素 - 通过
shapeFlag位运算判断节点类型
- 使用
至此,我们已经实现了一个简单的渲染器,掌握了 Vue 渲染机制的核心原理。