实现 render、patch、mountElement 和 unmount 的基础流程

2025-10-22 20:52:12

之前实现了 hcreateVNodeshapeFlag

现在已经可以创建出一个带有类型标记的 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 并更新
  • 卸载:新的 vnodenull,需要删除当前已经渲染的内容

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

挂载元素节点时,需要完成四步:

  1. 创建 DOM 节点
  2. 设置节点 props
  3. 挂载子节点
  4. 插入到指定容器
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。