实现 render、patch、mountElement 和 unmount 的基础流程
2025-10-22 20:52:12
之前实现了 h、createVNode 和 shapeFlag。
现在已经可以创建出一个带有类型标记的 vnode:
const vnode = h('div', 'hello world')
接下来就要回到之前留下的入口:createRenderer。
// packages/runtime-core/src/renderer.ts
export function createRenderer(renderOptions) {
const render = (vnode, container) => {
// 后面会在这里处理挂载和更新
}
return {
render,
createApp
}
}
render 的目标很直接:把 vnode 渲染到容器里。
但真正实现时,它至少要处理三件事:
- 挂载:之前没有旧
vnode,需要把新vnode挂载到 DOM 中 - 更新:之前已经有旧
vnode,需要对比新旧vnode并更新 - 卸载:新的
vnode是null,需要删除当前已经渲染的内容
render
render 是对外暴露的入口。
需要记住上一次渲染到容器里的 vnode,这样下一次调用时才能知道是挂载、更新还是卸载。
function render(vnode, container) {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode)
}
}
else {
patch(container._vnode || null, vnode, container)
}
container._vnode = vnode
}
将旧的 vnode 存到 container._vnode 上。
第一次渲染时,container._vnode 不存在,所以走挂载。
第二次渲染时,container._vnode 存在,所以走更新。
如果传入的是 null,就表示要把当前内容卸载掉。
patch
/**
*
* @param n1 旧 vnode
* @param n2 新 vnode
*
* 根据关系决定下一步的处理
*/
function patch(n1, n2, container) {
if (n1 === n2) {
return
}
if (n1 && !isSameVNodeType(n1, n2)) {
unmount(n1)
n1 = null
}
if (n1 == null) {
mountElement(n2, container)
}
else {
patchElement(n1, n2)
}
}
- 如果新旧 vnode 是同一个对象,直接返回
- 如果新旧 vnode 类型不同,先卸载旧节点
- 如果旧 vnode 不存在,说明这是挂载
- 如果旧 vnode 存在并且类型相同,说明这是更新
/**
* 判断两个 vnode 是否是统一个节点
*
* type 不同, 说明节点类型发生变化 div => span
* key 不同, 也要作为不同节点
*/
function isSameVNodeType(n1, n2) {
return n1.type === n2.type && n1.key === n2.key
}
unmount
卸载的目标,是根据 vnode 找到对应真实 DOM,然后从页面中删除。
如果当前 vnode 有数组子节点,需要先递归卸载子节点。
function unmount(vnode) {
const { shapeFlag, children } = vnode
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(children)
}
hostRemove(vnode.el)
}
function unmountChildren(children) {
for (let i = 0; i < children.length; i++) {
unmount(children[i])
}
}
mountElement
挂载元素节点时,需要完成四步:
- 创建 DOM 节点
- 设置节点 props
- 挂载子节点
- 插入到指定容器
function mountElement(vnode, container) {
const { type, props, children, shapeFlag } = vnode
const el = hostCreateElement(type)
vnode.el = el
if (props) {
for (const key in props) {
hostPatchProp(el, key, null, props[key])
}
}
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, children)
}
else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(el, children)
}
hostInsert(el, container)
}
挂载子节点
子节点有两种常见情况:
- 文本子节点
- 数组子节点
文本子节点可以直接设置:
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, children)
}
数组子节点则需要逐个挂载:
function mountChildren(container, children) {
for (let i = 0; i < children.length; i++) {
patch(null, children[i], container)
}
}
这里重新调用 patch(null, child, container)。
因为每个子节点自己也是 vnode,它也需要走同一套挂载流程。
createRenderer 的结构
把这些方法放回 createRenderer 里,大致就是这样:
export function createRenderer(options) {
const {
createElement: hostCreateElement,
insert: hostInsert,
remove: hostRemove,
setElementText: hostSetElementText,
patchProp: hostPatchProp
} = options
const render = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode)
}
}
else {
patch(container._vnode || null, vnode, container)
}
container._vnode = vnode
}
function patch(n1, n2, container) {
if (n1 === n2) {
return
}
if (n1 && !isSameVNodeType(n1, n2)) {
unmount(n1)
n1 = null
}
if (n1 == null) {
mountElement(n2, container)
}
else {
patchElement(n1, n2)
}
}
const mountElement = (vnode, container) => {
const { type, props, children, shapeFlag } = vnode
const el = hostCreateElement(type)
vnode.el = el
if (props) {
for (const key in props) {
hostPatchProp(el, key, null, props[key])
}
}
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, children)
}
else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(el, children)
}
hostInsert(el, container)
}
const mountChildren = (container, children) => {
for (let i = 0; i < children.length; i++) {
patch(null, children[i], container)
}
}
const unmount = (vnode) => {
const { shapeFlag, children } = vnode
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(children)
}
hostRemove(vnode.el)
}
const unmountChildren = (children) => {
for (let i = 0; i < children.length; i++) {
unmount(children[i])
}
}
return {
render
}
}
到这里,元素的首次挂载和卸载已经可以跑起来了。
小结
render 负责管理容器上的旧 vnode。
patch 负责判断当前应该挂载、更新还是卸载旧节点后重新挂载。
mountElement 负责把普通元素 vnode 变成真实 DOM。
unmount 负责根据 vnode 删除对应的真实 DOM。