Vue3.x下的渲染逻辑图示:

        如上图所示:

        在组件生命周期中,初次挂载会触发mounted钩子。后续如果状态发生变换,会触发beforeUpdate、updated钩子。这其实与渲染函数render有关。render函数首先会判断Vnode是否存在。

  • 如果不存在说明需要执行进行卸载,执行unmount操作。
  • 如果存在需要进行patch操作。patch的过程就包含了组件了创建到挂载,变化到更新。

        视图render中的patch的大体逻辑:

        源码:

const render = (vnode, container, isSVG) => {
    if (vnode == null) {
      // 如果没有Vnode,则卸载原来的Vnode
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 存在则对新旧Vnode进行patch
      // patch是一个递归的过程
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    // patch结束后,开始冲刷任务调度器中的任务
    flushPostFlushCbs()
    // 更新vnode
    container._vnode = vnode
  }
  1. 首先会判断Vnode是否存在,如果不存在,则调用unmount函数,进行组件的卸载
  2. 否则调用patch函数,对组件进行patch
  3. patch 结束后,会调用flushPostFlushCbs函数冲刷任务池
  4. 最后更新容器上的Vnode

patch方法详解

        Vnode有不同的类型,可以分为:

  • 简单类型:文本、注释、Static。
  • 复杂类型:组件、Fragment、Component、Teleport、Suspense。

        patch思路,可以看作一个深度优先遍历。

        简单类型就相当于JS中的原始数据类型:字符串、数字、布尔。

        复杂类型就相当于JS中的引用类型:对象、数组、Map、Set。

        不同的节点类型,需要采取不同的patch方式。

        而patch函数的主要职责就是去判断Vnode的节点类型(打上patchFlag标志),然后调用对应类型的Vnode处理方式,进行更细致的patch(最后进行render渲染)。

        源码:

const patch: PatchFn = (
  n1,
  n2,
  container,
  anchor = null,
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = false
) => {
  // patching & 不是相同类型的 VNode,则从节点树中卸载
  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) { // 根据 Vnode 类型判断
    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)
      }
      break
    case Fragment: // Fragment 类型
      processFragment(/* 忽略参数 */)
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) { // 元素类型
        processElement(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.COMPONENT) { // 组件类型
        processComponent(/* 忽略参数 */)
      } else if (shapeFlag & ShapeFlags.TELEPORT) { // TELEPORT 类型
        (type as typeof TeleportImpl).process(/* 忽略参数 */)
      }
  }
}

        patch 函数的源码分析:

  • n1 与 n2 是待比较的两个节点,n1 为旧节点,n2 为新节点。
  • container 是新节点的容器。
  • anchor 是一个锚点,用来标识当我们对新旧节点做增删或移动等操作时,以哪个节点为参照物。
  • optimized 参数是是否开启优化模式的标识。

        第一个 if 条件,当旧节点存在,并且新旧节点不是同一类型时,则将旧节点从节点树中卸载。也就是,当两个节点的类型不同,则直接卸载旧节点。

        再看第二个 if 分支条件,如果新节点的 patchFlag 的值是 BAIL ,优化模式会被关闭。

        接下来 patch 函数会通过 switch case 来判断节点类型,并分别对不同节点类型执行不同的操作。

1.Text(文本类型)

  • 匹配到Text类型Vnode。
  • 会调用ProcessText函数对节点进行处理。
  • ProcessText函数首先会判断n1是否存在。
  • 不存在,说明是第一次执行,直接进行文本插入。
  • 新旧,新旧文本不同,会设置新的Text。

2.Comment(注释类型) 

  • 匹配到Comment类型Vnode
  • 调用processCommentNode函数
  • 如果n1不存在,则执行插入工作
  • 否则直接新的覆盖旧的,因为注释节点并不需要在页面中进行展示,不必做多余的渲染工作

 3.Static(静态节点类型)

         Vue3的性能提升,有部分原因就是得益于对静态节点的处理。

  • patch过程中,匹配到Static类型节点。
  • 如果n1不存在,会调用mountStaticNode,对静态节点进行挂载操作。
  • 如果是dev环境,会调用patchStaticNode函数,patch节点。

        为什么仅在dev环境中进行patch呢,因为dev环境下涉及到HMR。

        另外静态节点不存在对state的依赖,不会触发track、trigger。且保持不变,在生产环境下,不必进行patch。以降低性能开销。

4.Fragment类型

        匹配到Fragment类型节点,调用processFragment函数,进行处理。

        Fragment节点,Fragment是Vue3中新增的Fragment组件,可以包裹多个子节点,但是并不会渲染Fragment节点。

        所以在渲染过程中主要处理的是Fragmemt包裹的子节点。

  • 如果n1不存在,会执行mountChildren,对子节点进行挂载。 mountChildren会对子节点进行遍历操作,递归调用patch函数。
  • 如果n1存在,会对子节点再进行进一步的判断
  • 如果patchFlag存在 && 存在动态节点 。则会调用patchBlockChildren,对子节点进行patch, patchBlockChildren会遍历子节点,递归调用patch函数
  • 否则会调用patchChildren函数,对子节点进行patch(diff算法进行比对)

 5.Element类型

        匹配到Element类型 ,调用processElement函数。

        n1不存在,表示是新增。会执行mountElement函数,对Vnode进行挂载。 mountElement在挂载Vnode过程中,会通过mountChildren,对子节点进行递归挂载处理。 并会对Vnode的prop进行patch。 并调用queuePostRenderEffect函数,向任务调度池中的后置执行阶段push生命周期钩子mounted。

        n1存在,会执行patchElement函数,对element进行patch,patchElement函数主要会执行以下任务:

  • 调用patchBlockChildren或者patchChildren进行patch操作
  • 并调用queuePostRenderEffect函数,向任务调度池中的后置执行阶段push生命周期钩子updated。 这里需要对子节点处理的原因是因为Element的子节点中,也可能还有组件或者其他类型的节点。
  • 调用hostPatchProp对节点的class、style进行patch
  • 遍历props对节点的新旧props进行patch

 6. Component类型

        通常情况下,我们都会给createApp传递一个组件

        故当render函数执行patch时,首先会匹配到组件类型的节点。如果是组件类型,会调用processComponent函数进行处理

  • 首先会判断n1是否存在,如果存在会进一步判断。
  • 会完成组件实例的创建
  • 完成Props、Slots的初始化
  • 执行setup函数,获取响应式状态
  • 完成组件模板的解析、编译与转换
  • 调用setupRenderEffect创建一个「渲染级别的effect」
  • 用于负责组件的更新,这里我暂时将其称为updateEffect。

        该组件是否是被Keep-Alive包裹的组件,如果是,则会执行组件的activate钩子。否则会调用mountComponent函数,对组件进行挂载

        mountComponent函数需要知道以下几点:

  • 主要会对组件的新旧Props、子节点进行判断
  • 如果发生变化,会调用mountComponent阶段创建的updateEffect,触发响应式系统
  • 否则直接原有的直接进行覆盖

7.Teleport类型 & Suspense类型

         Teleport 与 Suspense是Vue3新增的两个内置组件

        如果匹配到以上两种,会调用组件实例上的process方法 。porcess方法的主要逻辑与前面的相同 。首先会判断原有Vnode是否存在,不存在则mount,存在则patch

卸载组件

        如果调用render函数时没有传Vnode,则会调用unmount函数对组件进行卸载 。

        卸载过程中:

  • 如果存在ref,会首先重置ref
  • 如果组件是经过Keep-Alive缓存的组件,会通过deactivate对组件进行卸载
  • 如果是组件类型Vnode,会通过unmountComponent函数对组件进行卸载

        在卸载组件过程中会执行beforeMount生命周期钩子

        通过stop API来卸载组件的所有相关effect

        如果存在updateEffect,会卸载updateEffect,并递归调用unmount函数,对组件进行卸载 。最后会执行unmount生命周期钩子 。并通过*queuePostRenderEffect*向任务调度器中的后置任务池中,push一个用于标记组件已完成卸载的函数

        至此,就完成了组件的卸载工作

        如果不是组件类型的Vnode,会有以下几种情况:

  • 如果是Suspense类型,会通过Suspense实例上的unmount方法完成Vnode的卸载工作
  • 如果是Teleport类型,会通过Teleport实例上的remove方法完成Vnode的卸载工作
  • 如果存在子组件,会通过*unmountChildren*完成子组件的卸载工作

        最后会调用remove函数完成Fragment、Static、Element类型的卸载工作

        从上面整个过程可以看出,卸载组件过程基本与patch形似,也是对各种类型的Vnode有不同的处理方法,并会通过「递归」调用unmount完成组件的卸载工作,卸载过程中,会卸载组件相关的effect、updateEffect,触发卸载相关的生命周期钩子 & 指令相关的钩子。

Element的Patch过程

        上面的类型的处理都是调用各自的方法,基本上也就是进行了初步的比较,但是关于element的比较和Fragment类型的比较因为涉及到了后面的Diff算法比较DOM树差异所以在此进行单独叙述。

1.processElement()

        如patch()处理element类型元素流程图:

        n1存在,会执行patchElement函数,对element进行patch,patchElement函数主要会执行以下任务:

  • 调用patchBlockChildren或者patchChildren进行patch操作
  • 并调用queuePostRenderEffect函数,向任务调度池中的后置执行阶段push生命周期钩子updated。 这里需要对子节点处理的原因是因为Element的子节点中,也可能还有组件或者其他类型的节点。
  • 调用hostPatchProp对节点的class、style进行patch
  • 遍历props对节点的新旧props进行patch

        processElement源码:

const processElement = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  // 如果旧节点不存在
  if (n1 == null) {
    mountElement(
      n2,
      container,
      anchor
      /* 后续参数省略 */
    )
  } else {
    patchElement(
      n1,
      n2,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
}

2.patchElement()

        在元素类型的 patch 过程中,Vue3 首先会将新旧节点的 props 声明提取出来,因为之后需要对 props 进行 patch 比较。

        在比较开始之前会触发一些钩子,比如 VNode 自身的钩子:onVnodeBeforeUpdate,以及元素上绑定的指令的钩子 beforeUpdate。

if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
  invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
}
if (dirs) {
  invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
}

         之后开始比较 props,如果此时元素被标记过 patchFlag,则会通过 patchFlag 进行按需比较,否则会全量的 diff 元素中的 props。

if (patchFlag > 0) {
  if (patchFlag & PatchFlags.FULL_PROPS) {
    // 如果元素的 props 中含有动态的 key,则需要全量比较
    patchProps(
      el,
      n2,
      oldProps,
      newProps,
      parentComponent,
      parentSuspense,
      isSVG
    )
  } else {
    if (patchFlag & PatchFlags.CLASS) {
      if (oldProps.class !== newProps.class) {
        hostPatchProp(el, 'class', null, newProps.class, isSVG)
      }
    }

    if (patchFlag & PatchFlags.STYLE) {
      hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
    }

    if (patchFlag & PatchFlags.PROPS) {
      const propsToUpdate = n2.dynamicProps!
      for (let i = 0; i < propsToUpdate.length; i++) {
        const key = propsToUpdate[i]
        const prev = oldProps[key]
        const next = newProps[key]
        if (
          next !== prev ||
          (hostForcePatchProp && hostForcePatchProp(el, key))
        ) {
          hostPatchProp(
            el,
            key,
            prev,
            next,
            isSVG,
            n1.children as VNode[],
            parentComponent,
            parentSuspense,
            unmountChildren
          )
        }
      }
    }
  }

  if (patchFlag & PatchFlags.TEXT) {
    if (n1.children !== n2.children) {
      hostSetElementText(el, n2.children as string)
    }
  }
} else if (!optimized && dynamicChildren == null) {
  patchProps(
    el,
    n2,
    oldProps,
    newProps,
    parentComponent,
    parentSuspense,
    isSVG
  )
}
  • 当 patchFlag 为 FULL_PROPS 时,说明此时的元素中,可能包含了动态的 key ,需要进行全量的diff。
  • 当 patchFlag 为 CLASS 时,当新旧节点的 class 不一致时,此时会对 class 进行 patch,当新旧节点的 class 属性完全一致时,不需要进行任何操作。这个 Flag 标记会在元素有动态的 class 绑定时加入。
  • 当 patchFlag 为 STYLE 时,会对 style 进行更新,这是每次 patch 都会进行的,这个 Flag 会在有动态 style 绑定时被加入。
  • 当 patchFlag 为 PROPS 时,需要注意这个 Flag 会在元素拥有动态的属性或者 attrs 绑定时添加,不同于 class 和 style,这些动态的prop 或 attrs 的 key 会被保存下来以便于更快速的迭代。PROPS 的比较会将新节点的动态属性提取出来,并遍历这个这个属性中所有的 key,当新旧属性不一致,或者该 key 需要强制更新时,则调用 hostPatchProp 对属性进行更新。
  • 当 patchFlag 为 TEXT 时,如果新旧节点中的子节点是文本发生变化,则调用 hostSetElementText 进行更新。这个 flag 会在元素的子节点只包含动态文本时被添加。

        分支走到最后一个 else,若当前不存在优化标记,并且动态子节点也不存在,则直接对 props 进行全量 diff,通过 patchProps 这个函数完成。

3.patchChildren()

        接下来就会进入最重要的更新子节点的部分。在元素的 patch 过程中,会判断是否存在动态子节点,如果是则调用 patchBlockChildren 仅仅更新动态的子节点,否则会调用 patchChildren 对子节点进行全量更新。

        关于patchChildren()方法的详细解析请参考《Vue2.x以及3.x 核心Diff算法解析及源码》

总结:

        通过上面的梳理分析,可以知道,对于所有类型的组件,patch过程非常相似:

  1. 首先会判断原有的vnode是否存在。
  2. 如果不存在,则会进行mount操作。
  3. 如果存在则会对新旧Vnode进行patch操作。

        不同的是对于复杂类型的Vnode,由于其内部可能包含有其他类型的Vnode,比如Component类型。其中会涉及到:

  • 组件实例的创建
  • 模板的编译工作
  • 子组件的递归patch工作等等

        在unmount过程中,同样的会对不同的组件类型进行处理,并卸载组件的所有相关effect,递归卸载子组件。

        上面的两个过程中,都会向任务调度器中push任务。

        在render函数执行的最后阶段,会通过*flushPostFlushCbs*冲刷任务调度器。

参考及复制文章:

Logo

基于 Vue 的企业级 UI 组件库和中后台系统解决方案,为数万开发者服务。

更多推荐