微信图片_20211218203818.jpg

vue是如何组合起来的文章中我们知道虚拟DOM就是以对象形式描述的DOM,也了解了啥时候调用patch算法进行虚拟Dom比对。

但之前都是用简化版的去实现,具体怎么比对,Vnode长啥样,我们这就来学学。

再补充一下,变化侦测只通知到组件级别。为了减小性能浪费,这也是产生Vnode的一个原因。

Vnode长啥样

export interface VNode<
  HostNode = RendererNode,
  HostElement = RendererElement,
  ExtraProps = { [key: string]: any }
> {
  __v_isVNode: true
  [ReactiveFlags.SKIP]: true
  type: VNodeTypes
  props: (VNodeProps & ExtraProps) | null
  key: string | number | symbol | null
  ref: VNodeNormalizedRef | null
  scopeId: string | null
  slotScopeIds: string[] | null
  children: VNodeNormalizedChildren
  component: ComponentInternalInstance | null
  dirs: DirectiveBinding[] | null
  transition: TransitionHooks<HostElement> | null
  // DOM
  el: HostNode | null
  anchor: HostNode | null // fragment anchor
  target: HostElement | null // teleport target
  targetAnchor: HostNode | null // teleport target anchor
  staticCount: number
  suspense: SuspenseBoundary | null
  ssContent: VNode | null
  ssFallback: VNode | null
  shapeFlag: number
  patchFlag: number
  dynamicProps: string[] | null
  dynamicChildren: VNode[] | null
  appContext: AppContext | null
  memo?: any[]
  isCompatRoot?: true
  ce?: (instance: ComponentInternalInstance) => void
}

根据Vnode的定义,可见其就是一个普通的对象。

Vode的类型

export type VNodeTypes =
  | string
  | VNode
  | Component
  | typeof Text
  | typeof Static
  | typeof Comment
  | typeof Fragment
  | typeof TeleportImpl
  | typeof SuspenseImpl

不同类型的Vnode之间其实只是有效属性不同,创建Vnode实例时,无效的属性会有默认赋值

可以创建不同类型的Vnode

// 创建文本节点
export function createTextVNode(text: string = ' ', flag: number = 0): VNode {
  return createVNode(Text, null, text, flag)
}
// 创建静态节点
export function createStaticVNode(
  content: string,
  numberOfNodes: number
): VNode {
  const vnode = createVNode(Static, null, content)
  vnode.staticCount = numberOfNodes
  return vnode
}
// 创建注释节点
export function createCommentVNode(
  text: string = '',
  asBlock: boolean = false
): VNode {
  return asBlock
    ? (openBlock(), createBlock(Comment, null, text))
    : createVNode(Comment, null, text)
}

patch

终于到虚拟Dom的核心部分了

patch作用

对比两个Vnode之间的差异只是patch的一部分,这是手段,不是目的。patch是在现有的DOM上进行修改达到渲染视图的目的。所以要做三件事

  • 创建新增的节点
  • 删除废弃的节点
  • 修改更新的节点

看看源码

const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    if (n1 === n2) {
      return
    }

    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    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
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ;(type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }

    // set ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
    }
  }

新增节点

这通常发生在首次渲染中。因为在首次渲染中,oldVnode不存在任何节点。

还有一种情况是当oldVnode和vnode完全不是同一个节点时,vnode是一个全新的节点,而oldNode是废弃的节点。

// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
  anchor = getNextHostNode(n1)
  unmount(n1, parentComponent, parentSuspense, true)
  n1 = null
}
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)
      }
    }
  }

删除节点

在vnode中不存在的节点都是废弃的,需要删除

更新节点

静态节点

静态节点不会因状态的变化而发生变化,比对时可跳过更新操作

const patchStaticNode = (
  n1: VNode,
  n2: VNode,
  container: RendererElement,
  isSVG: boolean
) => {
  // static nodes are only patched during dev for HMR
  if (n2.children !== n1.children) {
    const anchor = hostNextSibling(n1.anchor!)
    // remove existing
    removeStaticNode(n1)
    // insert new
    ;[n2.el, n2.anchor] = hostInsertStaticContent!(
      n2.children as string,
      container,
      anchor,
      isSVG
    )
  } else {
    n2.el = n1.el
    n2.anchor = n1.anchor
  }
}
新虚拟节点有文本属性

当两个虚拟节点都不是静态节点时且有不同属性时,要以新的为准,如果新虚拟节点有文本属性可直接替换

新虚拟节点无文本属性

如果新虚拟节点无文本属性,两个都有children还要进行更细致的比对,若新节点无children,说明是空节点,将旧节点也变为空节点。

更新策略(diff算法)
创建子节点

当在oldChildren中没有找到与本次循环所指向的新子节点相同的节点,需要执行创建节点操作。

将创建的节点插入到oldChildren所以未处理的节点前面

移动子节点

当比对发现是同一节点但位置不同时,需要移动到所有未处理的节点前面

删除子节点

遍历后删除所有标记未处理的节点

优化策略

通常情况下,并不是所有子节点的位置都会发生移动,所以可以采用一下4种查找方式进行优化,注意查找的范围都是在未处理数组的前面

  • 新前与旧前

  • 新后与旧后

  • 新后与旧前

  • 新前与旧后

如果以上4种方式都不行再采用循环的方式

如何发现哪些节点是未处理的

建立四个变量:oldStartIdx、oldEndInx、newStartInx、newEndInx

while (oldStartInx <= oldEndInx && newStartInx <= newEndInx) {
  // 更新操作
}
if (newStartInx <= newEndInx) {
  // 范围内都是新增节点
}
if (oldStartInx <= oldEndInx) {
  // 范围内都是废弃节点
}

我觉得这哥们讲的挺好的

从createApp开始的首屏渲染

Vue3源码分析