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

304 阅读4分钟

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

前言

Vue 在通过 VNode 更新节点的时候,会对比新旧两个节点的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
) => {
  /** 忽略 */
  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
        )
      }
  }
}

processCommentNode 处理注释节点

当类型为 Comment 时,说明是注释节点。

const processCommentNode: ProcessTextOrCommentFn = (
  n1,
  n2,
  container,
  anchor
) => {
  if (n1 == null) {
    hostInsert(
      (n2.el = hostCreateComment((n2.children as string) || '')),
      container,
      anchor
    )
  } else {
    n2.el = n1.el
  }
}
  • 如果没有旧节点,则先创建一个注释节点,然后插入到 container 中
  • 因为 vue3 里面是不支持注释节点的动态替换的,所以当两个新旧节点都有数据的时候,是会直接把旧的内容直接赋予给新的节点。

StaticNode 静态节点

当类型为 Static 时,就是静态节点,如果没有旧节点,则直接通过mountStaticNode挂载新节点。

mountStaticNode = (
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  isSVG: boolean
) => {
//静态节点仅在与编译器dom/运行时dom一起使用时才存在
//这保证了hostInsertStaticContent的存在。
  ;[n2.el, n2.anchor] = hostInsertStaticContent!( // 创建静态节点,并且插入内容
    n2.children as string,
    container,
    anchor,
    isSVG
  )
}

否则进行 patch 更新,新旧节点的子节点不一致时,移除旧的的静态节点,插入新的静态节点。一致时,直接把旧节点复制给新节点

processFragment 片段

fragment 是 vue3 一个新组件,Vue2.x 时组件必须要一个根组件,在Vue3 中可以不用根标签,因为vue3会自动将多个节点用fragment包裹。

当旧节点n1不存在的时候,直接进行mountChildren操作,将内部的内容遍历挂载。 如果旧节点和新节点都有内容的情况下,就会把新旧节点的内容进行对比更新,由于可以接受v-for之类的动态内容,所以还将其进行不同的处理。

processElement 节点

processElement 这个方法本身的逻辑很简单,如果存在旧节点,则继续通过 patch 比较新旧两个节点,否则直接挂载新节点。关键在于 patch 两个节点的逻辑 patchElement。这个方法比较长,我们只摘出关键的 diff 算法部分

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,他的作用是优化了 patch 的过程。

当两个节点是同一类型时,比如都是 div,虽然类型相同,但是可能新节点的属性也发生了变化,所以此时我们还需要对节点的属性进行遍历,才能有效的判断是否需要更新。

<div id="bar" :class="foo">Hello World</div>

对于这么一个元素,id 和 文本都是静态的,不会发生改变,只有 class 是动态的,如果我们还是一样对新旧两个节点的 id、内容、class 都进行遍历对比, 会造成不必要的开销,因为只有class是动态的。而现在拥有了 patchFlag 在生成 AST 树后,经过转换器遍历各个节点时,就会根据节点的特点打上对应的 patchFlag。 而在 patch 的过程中,仅仅会处理 class 这一个 props,而并不是全量比较。这样的话能减少以及遍历 props 的次数,从而实现性能提升。

了解完 patchFlag 的作用,我们在回头看下patchElement的逻辑。

  • 当 patchFlag 为 FULL_PROPS 时,说明此时的元素的属性props中,可能包含了动态的 key ,需要进行全量的 props diff。
  • 当 patchFlag 为 CLASS 时,说明只有 class 是动态的,只对比新旧节点的 class ,当他们不一致时,会对 class 进行 patch,如果 class 属性完全一致时,不需要进行任何操作。这个 Flag 标记会在元素有动态的 class 绑定时加入。
  • 当 patchFlag 为 STYLE 时,会对 style 进行更新,这是每次 patch 都会进行的,这个 Flag 会在有动态 style 绑定时被加入。
  • 当 patchFlag 为 PROPS 时,需要注意这个 Flag 会在元素拥有动态的属性或者 attrs 绑定时添加,不同于 class 和 style,这些动态的prop 或 attrs 的 key 会被保存下来以便于更快速的迭代。
  • 当 patchFlag 为 TEXT 时,如果新旧节点中的子节点是文本发生变化,则进行更新。

小结

这节我们了解完了,patch 的流程,虽然后面还有 processComponent ,但是逻辑都是一样的,所以就不进行赘述。