Vue3 源码解析系列之初始化流程五 - patch

146 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

前言

上篇我们分析了VNode的生成,并研究了为什么 VNode 的标识符使用位操作进行判断。这篇主要是继续顺着初始化流程,createVNode 之后,执行了 createAppAPI 传入的render方法进行渲染。

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    mount(
      rootContainer: HostElement
    ): any {
      // 是否初始化挂载,只执行一次
      if (!isMounted) {
        const vnode = createVNode(
          rootComponent as ConcreteComponent,
          rootProps
        )
        vnode.appContext = context
        render(vnode, rootContainer, isSVG)
        isMounted = true
        return getExposeProxy(vnode.component!) || vnode.component!.proxy
      },
    }
  }
}

render

render 方法的位置是在 packages/runtime-core/src/renderer.ts 文件的 baseCreateRenderer 方法中定义的,并通过参数 传给了createAppAPI

// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(options: RendererOptions) {
  // 省略
  const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    // patch结束后,开始冲刷任务调度器中的任务
    flushPostFlushCbs()
    // 更新vnode
    container._vnode = vnode
  }
  // 省略
}

render 方法接收参数 vnode、container 和 isSVG。

  • 首先判断 vnode 是否存在。如果不存在,则调用unmount函数,进行组件的卸载
  • 如果存在需要进行patch操作,patch的过程就包含了组件了创建到挂载,变化到更新。
  • patch 结束后,会调用flushPostFlushCbs函数冲刷任务池
  • 最后更新容器上的 vnode

patch


/**
  n1 与 n2 是待比较的两个节点
  n1 为旧节点
  n2 为新节点
  container 是新节点的容器
  anchor 是一个锚点,用来标识当我们对新旧节点做增删或移动等操作时,以哪个节点为参照物。
  optimized 参数是是否开启优化模式的标识
*/
const patch: PatchFn = (
  n1,
  n2,
  container,
  anchor = null,
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
  // 当两个节点的类型不同,则直接卸载旧节点。
  if (n1 && !isSameVNodeType(n1, n2)) {
    anchor = getNextHostNode(n1)
    unmount(n1, parentComponent, parentSuspense, true)
    n1 = null
  }
  // 如果新节点的 patchFlag 的值是 BAIL ,优化模式会被关闭
  if (n2.patchFlag === PatchFlags.BAIL) {
    optimized = false
    n2.dynamicChildren = null
  }

  const { type, ref, shapeFlag } = n2
  switch (type) {
    case Text:
      processText(n1, n2, container, anchor)
      break
    case Comment:
      processCommentNode(n1, n2, container, anchor)
      break
    case Static:
      if (n1 == null) {
        mountStaticNode(n2, container, anchor, isSVG)
      } else if (__DEV__) {
        patchStaticNode(n1, n2, container, isSVG)
      }
      break
    case Fragment:
      processFragment(
        n1,
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      }
  }

}

patch 的逻辑比较多,我们逐一分析一下

  1. 传入的参数中 n1 与 n2 是待比较的两个节点,n1 为旧节点,n2 为新节点,container 是新节点的容器,anchor 是一个锚点,用来标识当我们对新旧节点做增删或移动等操作时,以哪个节点为参照物。optimized 参数是是否开启优化模式的标识
  2. 第一个条件,当旧节点存在,并且新旧节点不是同一类型时,则将旧节点从节点树中卸载。这就是我们可以总结出的 patch 的第一个逻辑: 当两个节点的类型不同,则直接卸载旧节点。
  3. 第二个条件,如果新节点的 patchFlag 的值是 BAIL ,优化模式会被关闭
  4. 接下来通过 switch 语句,判断新节点类型,分别对不同类型的节点进行操作

processText

我们来看看第一个,当新节点是文字时,会调用 processText

const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
  if (n1 == null) {
    hostInsert(
      (n2.el = hostCreateText(n2.children as string)),
      container,
      anchor
    )
  } else {
    const el = (n2.el = n1.el!)
    if (n2.children !== n1.children) {
      hostSetText(el, n2.children as string)
    }
  }
}

简单说一下 hostInsert 的功能是在 container 中的 anchor 之前插入节点 n2.el,就是调用了API container.insertBefore(n2.el, anchor);
hostCreateText 创建文本节点,调用了API createTextNode
hostSetText 为 el 设置文本

  • 首先,如果旧节点不存在时,说明是新添加元素,直接创建文本节点后,插入到容器中。
  • 如果旧节点存在,如果新旧节点不同,则用旧节点覆盖新节点。

小结

这节了解了,render 的逻辑,和 patch 的功能,并分析了文本节点的patch,下一篇我们继续分析其他节点的 patch