vue3源码学习3-DOM Diff

第2节中我们运行带副作用的渲染函数setupRenderEffect方法,上一节我们关注的是创建组件,这一节来看更新组件,文件位置:packages/runtime-core/src/renderer.ts

const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
	if (!instance.isMounted) {
		// 渲染组件
	} else {
		// 更新组件
		// next是新节点,vode是旧节点
		let { next, bu, u, parent, vnode } = instance
		if (next) {
          next.el = vnode.el
		  // 更新组件vnode节点信息
          updateComponentPreRender(instance, next, optimized)
        } else {
          next = vnode
        }
		// 渲染新的子树 vnode		
        const nextTree = renderComponentRoot(instance)
		// 缓存旧的子树 vnode
		const prevTree = instance.subTree
		// 更新子树 vnode
        instance.subTree = nextTree
		// 组件更新核心逻辑,根据新旧子树vnode做patch
		patch(
          prevTree,
          nextTree,
          // parent may have changed if it's in a teleport
		  // 如果在teleport组件中父节点可能已经改变,所以直接找旧树DOM元素的父节点
          hostParentNode(prevTree.el!)!,
          // anchor may have changed if it's in a fragment
		  // 参考节点在fragment的情况可能改变,多以直接找旧树DOM的下一个元素
          getNextHostNode(prevTree),
          instance,
          parentSuspense,
          isSVG
        )
		// 缓存更新后的DOM节点
		next.el = nextTree.el
	}
}

组件更新核心逻辑,patch流程:

const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
	// 如果存在新旧节点,且新旧节点类型不同,直接销毁旧节点,然后走mount流程
	if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
	  // n1设置为null,保证后续都走mount流程
      n1 = 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:
		// 处理Fragment元素
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
		  // 处理普通DOM元素
          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) {
		  // 处理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) {
		  // 处理SUSPENSE
          ;(type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        }
}

处理组件

  const processComponent = (
    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) {
		// 挂载组件
		mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
	} else {
		// 更新组件
		updateComponent(n1, n2, optimized)
	}

更新组件:

const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
	const instance = (n2.component = n1.component)!
	// 根据新旧子组件的vnode判断是否需要更新子组件
	if (shouldUpdateComponent(n1, n2, optimized)) {
		// 新的子组件 vnode 赋值给 instance.next
		instance.next = n2
		// 子组件可能因为数据变化被添加到更新队列里,移除他们防止子组件重复更新
		invalidateJob(instance.update)
		// 执行子组件的副作用渲染函数
		instance.update()
	} else {
		// 不需要更新,只复制属性
		n2.el = n1.el
	    instance.vnode = n2
	}

接下来我们回到副作用的渲染函数setupRenderEffect看下面的这行代码

// setupRenderEffect中的代码
updateComponentPreRender(instance, next, optimized)
// 看看这个方法都做了什么
const updateComponentPreRender = (
    instance: ComponentInternalInstance,
    nextVNode: VNode,
    optimized: boolean
  ) => {
	// 新组件vnode的component属性指向组件实例
    nextVNode.component = instance
	// 旧组件vnode 的props属性
    const prevProps = instance.vnode.props
	// 组件实例的vnode属性指向新的组件的vnode
    instance.vnode = nextVNode
	// 清空next属性,为了下一次重新渲染准备
    instance.next = null
	// 更新props
    updateProps(instance, nextVNode.props, prevProps, optimized)
	// 更新插槽
    updateSlots(instance, nextVNode.children, optimized)
}

处理普通元素

组件是抽象的,最终的更新还是会针对普通的DOM元素

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,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
	  // 更新元素
      patchElement(
        n1,
        n2,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
}

// 看patchElement的定义
const patchElement = (
    n1: VNode,
    n2: VNode,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
	const el = (n2.el = n1.el!)
	const oldProps = n1.props || EMPTY_OBJ
    const newProps = n2.props || EMPTY_OBJ
	
	const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
	// 更新子节点
	patchChildren(
        n1,
        n2,
        el,
        null,
        parentComponent,
        parentSuspense,
        areChildrenSVG,
        slotScopeIds,
        false
    )
	// 更新 props
	patchProps(
          el,
          n2,
          oldProps,
          newProps,
          parentComponent,
          parentSuspense,
          isSVG
     )
}

patchProps主要是更新DOM节点的class、style、event以及其他一些DOM属性,可以在packages/runtime-dom/src/patchProp.ts文件中自行查看,重点来看patchChildren方法:

const patchChildren: PatchChildrenFn = (
    n1,
    n2,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized = false
  ) => {
    const c1 = n1 && n1.children
    const prevShapeFlag = n1 ? n1.shapeFlag : 0
    const c2 = n2.children
    const { patchFlag, shapeFlag } = n2
	// children has 3 possibilities: text, array or no children.
	// 子节点有3种可能情况:文本、数组、空
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      // text children fast path
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
		// 数组变成文本,删除之前的子节点
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
      }
      if (c2 !== c1) {
		// 文本对比不相同,替换为新文本
        hostSetElementText(container, c2 as string)
      }
    } else {
	  // 之前的节点是数组
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // prev children was array
		// 新的节点也是数组
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          // two arrays, cannot assume anything, do full diff
		  // 数组到数组的更新,需要完整的diff
          patchKeyedChildren(
            c1 as VNode[],
            c2 as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else {
          // no new children, just unmount old
		  // 没有新的子节点,删除之前的子节点
          unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
        }
      } else {
        // prev children was text OR null
        // new children is array OR null
		// 之前的子节点是文本节点或空, 新的子节点是数组或空
        if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
		  // 之前的子节点是文本,则清空
          hostSetElementText(container, '')
        }
        // mount new if array
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
		  // 如果新的子节点是数组,挂载新子节点
          mountChildren(
            c2 as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        }
      }
    }
  }

一个元素的子节点vnode有三种情况:文本、vnode数组和空,排列组合有九种情况:

旧子节点 新子节点 操作
纯文本 纯文本 替换新文本
删除旧子节点
vnode数组 清空文本,添加多个新子节点
纯文本 添加新文本节点
什么都不做
vnode数组 添加多个新子节点
vnode数组 纯文本 删除旧子节点,添加新文本节点
删除旧子节点
vnode数组 完成的diff子节点

核心diff算法。。。

posted @ 2022-09-01 17:48  菜菜123521  阅读(80)  评论(0编辑  收藏  举报