Diff算法

75 阅读11分钟

虚拟DOM

虚拟DOMvirtual DOM)用js对象结构表示DOM树结构,然后用这个树构建一个真正的DOM树,插到文档中,当状态变更时,重新构造一颗新的对象树,然后将新的树和旧的树进行比较,记录差异,并把差异以打补丁的方式应用到真正的DOM树上来更新视图。

diff算法就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。原始的diff算法是遍历循环比较的,不分层级的,而优化后的diff算法同层比较,不跨层级的。

b2de9c82d158ccbf9babc0f9c1bc2637b0354148.webp

vue中的diff算法

data发生变化时,set方法会调用Dep.notify通知所有订阅者,此时订阅者会调用patch方法给dom打补丁来更新视图:

42166d224f4a20a4b0a5c4d64d36022b730ed0d3.webp

patch方法

function patch (oldVnode, vnode) {
    // ......
    if (sameVnode(oldVnode, vnode)) { // 节点一样的话深入检查子节点
    	patchVnode(oldVnode, vnode)
    } else {
    	const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
    	let parentEle = api.parentNode(oEl)  // 父元素
    	createEle(vnode)  // 根据Vnode生成新元素
    	if (parentEle !== null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
            api.removeChild(parentEle, oldVnode.el)  // 移除以前的旧元素节点
            oldVnode = null
    	}
    }
    // ......
    return vnode
}

该方法接受两个参数分别是旧节点oldVnode和新节点Vnode。用sameVnode方法判断两个节点是否可以进行比较,如果可以的话调用patchVnode方法,否则直接用Vnode替换oldVnode

function sameVnode (a, b) {
  return (
    a.key === b.key &&  // key值
    a.tag === b.tag &&  // 标签名
    a.isComment === b.isComment &&  // 是否为注释节点
    // 是否都定义了data,data包含一些具体信息,例如onclick , style
    isDef(a.data) === isDef(b.data) &&  
    sameInputType(a, b) // 当标签是<input>的时候,type必须相同
  )
}

patchVnode方法

function patchVnode (
    oldVnode,  // 旧节点
    vnode,     // 新节点
    insertedVnodeQueue,  // 插入节点的队列
    ownerArray,      // 节点 数组
    index,           // 当前 节点的
    removeOnly       // 只有在 patch 函数中被传入,当老节点不是真实的 dom 节点,当新老节点是相同节点的时候
  ) {
    // 如果新节点和旧节点 相等(使用了 同一个地址,直接返回不进行修改)
    // 这里就是 当 props 没有改变的时候,子组件不会做渲染,而是直接复用
    if (oldVnode === vnode) {
      return
    }
 
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }
 
    const elm = vnode.elm = oldVnode.elm
    // 当 当前节点 是 注释节点(被 v-if )了,或者是一个 异步函数节点,那不执行
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
 
    // 当前节点 是一个静态节点的时候,或者 标记了 once 的时候,那不执行
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }
 
    let i
    const data = vnode.data
    // 调用 prepatch 的钩子函数
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }
 
    const oldCh = oldVnode.children
    const ch = vnode.children
    // 调用 update 钩子函数
    if (isDef(data) && isPatchable(vnode)) {
      // 这里 的 update 钩子函数式 vnode 本身的钩子函数
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // 这里的 update 钩子函数  是 用户传过来的 钩子函数
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 新节点 没有 text 属性
    if (isUndef(vnode.text)) {
      // 如果都有子节点,对比更新子节点
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) { // 新节点存在子节点,但是老节点不存在子节点
        // 如果老节点是  text, 清空
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // 增加子节点
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) { // 老节点存在子节点,但是新节点不存在子节点,执行删除
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) { // 如果老节点是  text, 清空
        nodeOps.setTextContent(elm, '')
      }
       // 新旧节点 text 属性不一样
    } else if (oldVnode.text !== vnode.text) {
      // 将 text 设置为 新节点的 text
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      // 执行 postpatch 钩子函数
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

该方法会首先看是否是同一个节点、是否是注释节点、是否是异步函数组件、是否是静态节点,是否是 once 的节点,是的话,不执行;接着查看是否存在子节点,新老节点都存在子节点,就执行updateChildren函数深入对比;新节点存在子节点但是老节点不存在子节点,增加,新节点不存在子节点但是老节点存在子节点,删除,检查新老节点的text属性是否一致,否的话,就更新。

updateChildren (parentElm, oldCh, newCh) {
    let oldStartIdx = 0, newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx
    let idxInOld
    let elmToMove
    let before
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (oldStartVnode == null) {   // 对于vnode.key的比较,会把oldVnode = null
            oldStartVnode = oldCh[++oldStartIdx] 
        }else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx]
        }else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx]
        }else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        }else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode)
            api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode)
            api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        }else {
           // 使用key时的比较
            if (oldKeyToIdx === undefined) {
                oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
            }
            idxInOld = oldKeyToIdx[newStartVnode.key]
            if (!idxInOld) {
                api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                newStartVnode = newCh[++newStartIdx]
            }
            else {
                elmToMove = oldCh[idxInOld]
                if (elmToMove.sel !== newStartVnode.sel) {
                    api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                }else {
                    patchVnode(elmToMove, newStartVnode)
                    oldCh[idxInOld] = null
                    api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
                }
                newStartVnode = newCh[++newStartIdx]
            }
        }
    }
    if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
    }else if (newStartIdx > newEndIdx) {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
}

该方法会从双端比较新旧节点的各个子节点,一共有四种比较方法,如果四种比较都不匹配并且设置了key值,那么就会用key值进行比较。

未命名文件 (8).png

  • oldSS匹配,真实DOM的节点不变。(对应的两个指针都向中间靠)
  • oldEE匹配,真实DOM的节点不变。
  • oldSE匹配,真实DOM的第一个节点移到最后。
  • oldES匹配,真实DOM的最后一个节点移到最前方。
  • 如果新旧子节点都存在key,那么会根据oldChildkey生成一张hash表,用Skeyhash表做匹配,匹配成功就判断S和匹配节点是否为sameNode,如果是,就在真实dom中将成功的节点移到最前面,否则,将S生成对应的节点插入到dom中对应的oldS位置,S指针向中间移动,被匹配old中的节点置为null
  • 如果没有key,则直接将S生成新的节点插入真实DOM(有了key,可以尽可能地重用元素,主要是为了高效地更新虚拟DOM。)

例子

未命名文件 (7).png

真实DOM: b d c a

old: b d c a

new: a e b f
  1. oldS = b, oldE = a;
    S = a, E = f;
    

    oldE与S匹配,将a节点放到第一个,此时真实DOM:a b d c;

  2. oldS = b, oldE = c;
    S = e, E = f;
    

    四种匹配都不行,看key值,将e节点移到oldS所在的位置,此时真实DOM:a e b d c;

  3. oldS = b, oldE = c;
    S = b, E = f;
    

    oldS与S匹配,节点不变,此时真实DOM:a e b d c;

  4. oldS = d, oldE = c;
    S = f, E = f;
    

    四种匹配都不行,看key值,将f节点移到oldS所在的位置,此时真实DOM:a e b f d c;

  5. S > E,结束比较,删除旧节点,此时真实DOM:a e b f;

react中的diff算法

Fiber双缓存

react的源码主要分为三个部分:

  • Scheduler(调度器):根据任务优先级,从高到低依次安排任务进行reconcile;(16之前没有这一个部分。任务只能同步执行,不能中断)
  • Reconciler(协调器):找出改变的节点,打上增删改的effectTag
  • Renderer(渲染器):将打上effectTag的节点渲染到视图上;

未命名文件.png

Fiber是保存DOM信息的一种数据结构,通过childsiblingreturn形成Fiber树。

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 作为静态数据结构的属性
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 用于连接其他Fiber节点形成Fiber树
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  // 作为动态的工作单元的属性
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  this.effectTag = NoEffect;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该fiber在另一次更新时对应的fiber
  this.alternate = null;
}

举个例子,如果一个组件是以下结构:

function App() {
  return (
  	<div>
        mm,
    	<ul>
      		<li>hello</li>
      		<li>i'm wgg</li>
    	</ul>
    </div>
  )
}

那么对应的Fiber树结构为:

未命名文件 (2).png

react中存在两棵Fiber树:current Fiber树描述了当前展现的DOM树,workInProgress Fiber树是正在更新的Fiber树。

首次渲染的话,直接根据jsx返回的对象构建current Fiber树应用到真实DOM上,更新的话,会将jsx返回的对象与current Fiber树做比较形成workInProgress Fiber树,然后workInProgress Fiber树变成current Fiber树应用到真实DOM上。这个对比的过程就叫diff算法。

diff算法

经典diff算法的时间复杂度为O(n^3),而react通过三个策略将diff算法优化到 O(n) 的时间复杂度:

  • 同级元素比较,跨级层的不复用。
  • 不同类型节点生成的DOM树不同,此时会直接销毁老节点及子孙节点,并新建节点。
  • 对同一层级的子节点,开发者可以通过 key 来确定哪些子元素可以在不同渲染中保持稳定。
export function reconcileChildren(
  current: Fiber | null, // 当前 fiber 节点
  workInProgress: Fiber, // 父 fiber 节点
  nextChildren: any, // 新生成的 ReactElement 内容
  renderLanes: Lanes, // 渲染的优先级
) {
  if (current === null) {
    // 如果当前 fiber 节点为空,则直接将新的 ReactElement 内容生成新的 fiber
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 当前 fiber 节点不为空,则与新生成的 ReactElement 内容进行 diff
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

该方法根据当前Fiber是否存在决定是否直接渲染ReactElement内容或者是否与current Fiber树做比较进行diff

function reconcileChildFibers(
  returnFiber: Fiber, // 父 Fiber
  currentFirstChild: Fiber | null, // 父 fiber 下要对比的第一个子 fiber
  newChild: any, // 更新后的 React.Element 内容
  lanes: Lanes, // 更新的优先级
): Fiber | null {

  // 对新创建的 ReactElement 最外层是 fragment 类型单独处理,比较其 children
  const isUnkeyedTopLevelFragment =
    typeof newChild === 'object' &&
    newChild !== null &&
    newChild.type === REACT_FRAGMENT_TYPE &&
    newChild.key === null;
  if (isUnkeyedTopLevelFragment) {
    newChild = newChild.props.children;
  }

  // 对更新后的 React.Element 是单节点的处理
  if (typeof newChild === 'object' && newChild !== null) {
    switch (newChild.$$typeof) {
      // 常规 react 元素
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(
          reconcileSingleElement(
            returnFiber,
            currentFirstChild,
            newChild,
            lanes,
          ),
        );
      // react.portal 类型
      case REACT_PORTAL_TYPE:
        return placeSingleChild(
          reconcileSinglePortal(
            returnFiber,
            currentFirstChild,
            newChild,
            lanes,
          ),
        );
      // react.lazy 类型
      case REACT_LAZY_TYPE:
        if (enableLazyElements) {
          const payload = newChild._payload;
          const init = newChild._init;
          return reconcileChildFibers(
            returnFiber,
            currentFirstChild,
            init(payload),
            lanes,
          );
        }
    }

    // 更新后的 React.Element 是多节点的处理
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }

    // 迭代器函数的单独处理
    if (getIteratorFn(newChild)) {
      return reconcileChildrenIterator(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }

    throwOnInvalidObjectType(returnFiber, newChild);
  }

  // 纯文本节点的类型处理
  if (typeof newChild === 'string' || typeof newChild === 'number') {
    return placeSingleChild(
      reconcileSingleTextNode(
        returnFiber,
        currentFirstChild,
        '' + newChild,
        lanes,
      ),
    );
  }

  if (__DEV__) {
    if (typeof newChild === 'function') {
      warnOnFunctionType(returnFiber);
    }
  }

  // 不符合以上情况都视为 empty,直接从父节点删除所有旧的子 Fiber
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

该方法主要根据节点类型的不同来进行不同的处理,我们主要讲一下一下几个类型:

  • 单节点diff(同级只有一个节点)

    function reconcileSingleElement(
      returnFiber: Fiber, // 父 fiber
      currentFirstChild: Fiber | null, // 父 fiber 下第一个开始对比的旧的子  fiber
      element: ReactElement, // 当前的 ReactElement内容
      lanes: Lanes, // 更新的优先级
    ): Fiber {
      const key = element.key;
      let child = currentFirstChild;
      // 处理旧的 fiber 由多个节点变成新的 fiber 一个节点的情况
      // 循环遍历父 fiber 下的旧的子 fiber,直至遍历完或者找到 key 和 type 都与新节点相同的情况
      while (child !== null) {
        if (child.key === key) { // key相同
          const elementType = element.type;
          if (elementType === REACT_FRAGMENT_TYPE) {
    		//...... 
          } else {
            if (
              // 如果新的 ReactElement 和旧 Fiber 的 key 和 type 都相等
              child.elementType === elementType ||
              (__DEV__
                ? isCompatibleFamilyForHotReloading(child, element)
                : false) ||
              (enableLazyElements &&
                typeof elementType === 'object' &&
                elementType !== null &&
                elementType.$$typeof === REACT_LAZY_TYPE &&
                resolveLazy(elementType) === child.type)
            ) {
              // 对旧 fiber 后面的所有兄弟节点添加 Deletion 副作用标记,用于 dom 更新时删除
              deleteRemainingChildren(returnFiber, child.sibling);
              // 通过 useFiber 复用新节点并返回
              const existing = useFiber(child, element.props);
              existing.ref = coerceRef(returnFiber, child, element);
              existing.return = returnFiber;
              if (__DEV__) {
                existing._debugSource = element._source;
                existing._debugOwner = element._owner;
              }
              return existing;
            }
          }
          // 若 key 相同但是 type 不同说明不匹配,移除旧 fiber 及其后面的兄弟 fiber
          deleteRemainingChildren(returnFiber, child);
          break;
        } else {
          // 若 key 不同,对当前的旧 fiber 添加 Deletion 副作用标记,继续对其兄弟节点遍历
          deleteChild(returnFiber, child);
        }
        child = child.sibling;
      }
    
      // 都遍历完之后说明没有匹配到 key 和 type 都相同的 fiber
      if (element.type === REACT_FRAGMENT_TYPE) {
        // ......
      } else {
        // createFiberFromElement 创建 fiber 并返回
        const created = createFiberFromElement(element, returnFiber.mode, lanes);
        created.ref = coerceRef(returnFiber, currentFirstChild, element);
        created.return = returnFiber;
        return created;
      }
    }
    
    • Fiber和新内容(jsx形成的vDom,以下称vDomkeytype相同,可以复用节点。

      FibervDomkey相同type不同,节点及其兄弟节点都不能复用,打上Deletion标签,创建新的节点,打上 Placement 的标记。

    • FibervDomkeytype都不相同,直接打上删除Deletion标签,创建新的节点,打上 Placement 的标记。

  • ​ 多节点diff(同级有多个节点)

    function reconcileChildrenArray(
      returnFiber: Fiber,  currentFirstChild: Fiber | null,  newChildren: Array<*>,  lanes: Lanes,
    ): Fiber | null {
      // ......
    
      let resultingFirstChild: Fiber | null = null; // 最终要返回的第一个子 fiber
      let previousNewFiber: Fiber | null = null;
    
      let oldFiber = currentFirstChild;
      let lastPlacedIndex = 0;
      let newIdx = 0;
      let nextOldFiber = null;
      // 因为在实际的应用开发中,react 发现更新的情况远大于新增和删除的情况,所以这里优先处理更新
      // 根据 oldFiber 的 index 和 newChildren 的下标,找到要对比更新的 oldFiber
      for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
        if (oldFiber.index > newIdx) {
          nextOldFiber = oldFiber;
          oldFiber = null;
        } else {
          nextOldFiber = oldFiber.sibling;
        }
        // 通过 updateSlot 来 diff oldFiber 和新的 child,生成新的 Fiber
        // updateSlot 与上面单节点的 diff 类似,如果 oldFiber 可复用,则根据 oldFiber 和 child 的 props 生成新的 fiber;否则返回 null
        const newFiber = updateSlot(
          returnFiber,
          oldFiber,
          newChildren[newIdx],
          lanes,
        );
        // newFiber 为 null 说明不可复用,退出第一轮的循环
        if (newFiber === null) {
          if (oldFiber === null) {
            oldFiber = nextOldFiber;
          }
          break;
        }
        if (shouldTrackSideEffects) {
          if (oldFiber && newFiber.alternate === null) {
            deleteChild(returnFiber, oldFiber);
          }
        }
        // 记录复用的 oldFiber 的 index,同时给新 fiber 打上 Placement 副作用标签
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    
        if (previousNewFiber === null) {
          // 如果上一个 newFiber 为 null,说明这是第一个生成的 newFiber,设置为 resultingFirstChild
          resultingFirstChild = newFiber;
        } else {
          // 否则构建链式关系
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
        oldFiber = nextOldFiber;
      }
    
      if (newIdx === newChildren.length) {
        // newChildren遍历完了,说明剩下的 oldFiber 都是待删除的 Fiber
        // 对剩下 oldFiber 标记 Deletion
        deleteRemainingChildren(returnFiber, oldFiber);
        return resultingFirstChild;
      }
    
      if (oldFiber === null) {
        // olderFiber 遍历完了
        // newChildren 剩下的节点都是需要新增的节点
        for (; newIdx < newChildren.length; newIdx++) {
          // 遍历剩下的 child,通过 createChild 创建新的 fiber
          const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
          if (newFiber === null) {
            continue;
          }
          // 处理dom移动,// 记录 index,同时给新 fiber 打上 Placement 副作用标签
          lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
          // 将新创建 fiber 加入到 fiber 链表树中
          if (previousNewFiber === null) {
            resultingFirstChild = newFiber;
          } else {
            previousNewFiber.sibling = newFiber;
          }
          previousNewFiber = newFiber;
        }
        return resultingFirstChild;
      }
    
      // oldFiber 和 newChildren 都未遍历完
      // mapRemainingChildren 生成一个以 oldFiber 的 key 为 key, oldFiber 为 value 的 map
      const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    
      // 对剩下的 newChildren 进行遍历
      for (; newIdx < newChildren.length; newIdx++) {
        // 找到 mapRemainingChildren 中 key 相等的 fiber, 创建新 fiber 复用
        const newFiber = updateFromMap(
          existingChildren,
          returnFiber,
          newIdx,
          newChildren[newIdx],
          lanes,
        );
        if (newFiber !== null) {
          if (shouldTrackSideEffects) {
            if (newFiber.alternate !== null) {
              // 删除当前找到的 fiber
              existingChildren.delete(
                newFiber.key === null ? newIdx : newFiber.key,
              );
            }
          }
          // 处理dom移动,记录 index,同时给新 fiber 打上 Placement 副作用标签
          lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
          // 将新创建 fiber 加入到 fiber 链表树中
          if (previousNewFiber === null) {
            resultingFirstChild = newFiber;
          } else {
            previousNewFiber.sibling = newFiber;
          }
          previousNewFiber = newFiber;
        }
      }
    
      if (shouldTrackSideEffects) {
        // 剩余的旧 fiber 的打上 Deletion 副作用标签
        existingChildren.forEach(child => deleteChild(returnFiber, child));
      }
    
      return resultingFirstChild;
    }
    

    从源码里可以看到多节点diff要经历3个遍历:

    • 先对newChildren进行第一轮遍历,将当前的 oldFiber 与 当前 newIdx 下标的 newChild 通过 updateSlot进行 diffdiff的流程和上面单节点的 diff 类似,然后返回 diff 后的结果:
      • 如果 diffoldFibernewIdxkeytype 一致,说明可复用。根据 oldFibernewChildprops 生成新的 fiber,通过 placeChild 给新生成的 fiber 打上 Placement 副作用标记,同时新 fiber 与之前遍历生成的新 fiber 构建链表树关系。然后继续执行遍历,对下一个 oldFiber 和下一个 newIdx 下标的 newFiber 继续 diff
      • 如果 diffoldFibernewIdxkeytype 不一致,那么说明不可复用,返回的结果为 null,第一轮遍历结束
    • 第一轮遍历结束后,可能会执行以下几种情况:
      • newChildren 遍历完了,那剩下的 oldFiber 都是待删除的,对剩下的 oldFiber 打上 Deletion 副作用标记
      • oldFiber 遍历完了,那剩下的 newChildren 都是需要新增的,遍历剩下的 newChildren, 创建新的 fiber,给新生成的 fiber 打上 Placement 副作用标记并添加到 fiber 链表树中。
      • oldFibernewChildren 都未遍历完,通过 mapRemainingChildren 创建一个以剩下的 oldFiberkeykeyoldFibervaluemap。然后对剩下的 newChildren 进行遍历,通过 updateFromMapmap 中寻找具有相同 key 创建新的fiber(若找到则基于 oldFibernewChildprops创建,否则直接基于 newChild 创建),则从 map 中删除当前的 key,然后placeChild 给新生成的 fiber 打上 Placement 副作用标记并添加到 fiber 链表树中。遍历完之后则 existingChildren 还剩下 oldFiber 的话,则都是待删除的 fiberdeleteChild 对其打上 Deletion 副作用标记。

简单来讲:

第一轮遍历,一一对比 vdom 和老的 fiber,如果可以复用就处理下一个节点,否则就结束遍历。

如果所有的新的 vdom 处理完了,那就把剩下的老 fiber 节点删掉就行。

如果还有 vdom 没处理,那就进行第二次遍历:

第二轮遍历,把剩下的老 fiber 放到 map 里,遍历剩下的 vdom,从 map 里查找,如果找到了,就移动过来。

第二轮遍历完了之后,把剩余的老 fiber 删掉,剩余的 vdom 新增。

例子

未命名文件 (4).png

一一对比新的 vdom 和 老的 fiber,发现b是可以复用的,那就创建新 fiber 节点,打上更新标记。

c不可复用,所以结束第一轮遍历,进入第二轮遍历。

未命名文件 (6).png

e在map中无法匹配,所以直接创建一个新的fiber,打上新增的标签,d可以在map中找到,所以直接复用,打上更新的标签,此时vDom已经遍历完毕,还剩下c,打上删除的标签。