likes
comments
collection
share

react fiber 中的dom diff

作者站长头像
站长
· 阅读数 15

两种节点类型

我们可以从同级的节点数量将Diff分为两类:

当newChild类型为object、number、string,代表同级只有一个节点

当newChild类型为Array,同级有多个节点

在接下来两节我们会分别讨论这两类节点的Diff,注意这里的单节点是指虚拟dom节点是个单或者多节点,可以简单看做是不是返回的数组

单节点

单节点比较还是比较简单的

//删除节点
  function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
    if (!shouldTrackSideEffects) {
      // Noop.
      return;
    }
//effect链的处理
    const last = returnFiber.lastEffect;
    if (last !== null) {
      last.nextEffect = childToDelete;
      returnFiber.lastEffect = childToDelete;
    } else {
        //证明暂时还没有形成链需要第一个节点
      returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
    }
    const deletions = returnFiber.deletions;
    if (deletions === null) {
      returnFiber.deletions = [childToDelete];
      // TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions)
      returnFiber.effectTag |= Deletion;
    } else {
      deletions.push(childToDelete);
    }
    childToDelete.nextEffect = null;
  }

//批量删除节点的工具函数(更准确的是批量标记)
  function deleteRemainingChildren(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
  ): null {
    if (!shouldTrackSideEffects) {
      // Noop.
      return null;
    }

    // TODO: For the shouldClone case, this could be micro-optimized a bit by
    // assuming that after the first child we've already added everything.
    let childToDelete = currentFirstChild;
    while (childToDelete !== null) {
      deleteChild(returnFiber, childToDelete);
      childToDelete = childToDelete.sibling;
    }
    return null;
  }


//element其实就是新的虚拟dom 
function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement
): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  
  // 首先判断是否存在对应DOM节点
  while (child !== null) {
    // 上一次更新存在DOM节点,接下来判断是否可复用

    // 首先比较key是否相同
    if (child.key === key) {

      // key相同,接下来比较type是否相同

      switch (child.tag) {
        // ...省略case
        
        default: {
          if (child.elementType === element.type) {
            // type相同则表示可以复用
            deleteRemainingChildren(returnFiber, child.sibling);//显然这个节点的后续节点都必须删除了 因为找到了
            const existing = useFiber(child, element.props);//useFiber故名思义 这里的element.props就是后续看是否要调整的属性
            // 返回复用的fiber
            return existing;
          }
          
          // type不同则跳出switch
          break;
        }
      }
      // 代码执行到这里代表:key相同但是type不同
      // 将该fiber及其兄弟fiber标记为删除
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      // key不同,将该fiber标记为删除
      deleteChild(returnFiber, child);
    }
    child = child.sibling;
  }

  // 创建新Fiber,并返回 ...省略
}

可以发现需要被删除的fiber 不会在这直接真的删除,而是形成一个effect链,另外父节点会维护一个deletions的fiber数组

首先判断child是否存在,不存在则直接开始兄弟节点的比较,while终止在同层比较完成后几种逻辑分支

  1. key相同,类型也相同直接可复用,后续就看属性情况更新属性即可
  2. key相同,类型不同了,直接deleteRemainingChildren 删除这个节点及他的兄弟节点,这里是因为key相同了,后续没有继续比较找可复用节点的意义了,故把原节点删完就可以了
  3. key不同,直接把这个比较的节点删除

多节点

先整理一下源码

  function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ): Fiber | null {

    let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;

    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
          // We matched the slot, but we didn't reuse the existing fiber, so we
          // need to delete the existing child.
          deleteChild(returnFiber, oldFiber);
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        //构建新的fiber链作为返回值
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    if (newIdx === newChildren.length) {
      // We've reached the end of the new children. We can delete the rest.
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }

    if (oldFiber === null) {
      // If we don't have any more existing children we can choose a fast path
      // since the rest will all be insertions.
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {//构建新的fiber链作为返回值
          // TODO: Move out of the loop. This only happens for the first run.
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
    }

    // Add all children to a key map for quick lookups.
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // Keep scanning and use the map to restore deleted items as moves.
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            existingChildren.delete(
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

    if (shouldTrackSideEffects) {
      // Any existing children that weren't consumed above were deleted. We need
      // to add them to the deletion list.
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }

    return resultingFirstChild;
  }

先对几个用到的重要函数解读一下updateSlot这个函数可以简单理解为节点比较,如果不匹配返回null,不然就是一个可复用的fiber节点

  function mapRemainingChildren(
    returnFiber: Fiber,
    currentFirstChild: Fiber,
  ): Map<string | number, Fiber> {
    const existingChildren: Map<string | number, Fiber> = new Map();

    let existingChild = currentFirstChild;
    while (existingChild !== null) {
      if (existingChild.key !== null) {
        existingChildren.set(existingChild.key, existingChild);
      } else {
        existingChildren.set(existingChild.index, existingChild);
      }
      existingChild = existingChild.sibling;
    }
    return existingChildren;
  }

mapRemainingChildren返回一个children构成的Map key为id或者是child的index

内容比较多建议分成三个步骤去看

  1. 第一个循环,比较newChildren 和oldFiber和他的兄弟们 会有三种情况

    • key不同循环直接停止
    • key相同,类型不同,fiber标记为删除循环继续 i++ oldFiber = nextOldFiber;
    • 循环结束newChildren或者是oldFiber和他的兄弟们遍历结束了
  2. 循环完处理一下几种情况 ,一种是newChildren现遍历完了,那删除剩余的oldFiber,deleteRemainingChildren(returnFiber, oldFiber); 第二种是oldFiber遍历完了,那剩余的newChildren 需要创建fiber节点 并且拼接在previousNewFiber这个结果链上 触发这两种情况都会退出整个diff
  3. 也就是都没有遍历完,情况就是由于节点位置移动导致的,这个时候先要mapRemainingChildren(returnFiber, oldFiber);把剩余的fiber做一个Map映射,然后newChildren 剩余的节点去Map中查找,重点是placeChild函数
  function placeChild(
    newFiber: Fiber,
    lastPlacedIndex: number,
    newIndex: number,
  ): number {
    newFiber.index = newIndex;
    if (!shouldTrackSideEffects) {
      // Noop.
      return lastPlacedIndex;
    }
    const current = newFiber.alternate;
    if (current !== null) {
      const oldIndex = current.index;
      if (oldIndex < lastPlacedIndex) {
        // This is a move.
        //原本节点的index比当前最近一次替换过的节点的index还小的话标记为移动,且lastPlacedIndex不变
        newFiber.effectTag = Placement;
        return lastPlacedIndex;
      } else {
        // This item can stay in place.
        //返回原有节点的位置作为新的lastPlacedIndex
        return oldIndex;
      }
    } else {
      // This is an insertion.
        //newChildren在原本没有 完全是新建的
      newFiber.effectTag = Placement;
      return lastPlacedIndex;
    }
  }

举个例子如果 01234要变为12304 假设lastPlacedIndex为0初始开始循环1 oldIndex为1 oldIndex > lastPlacedIndex 不动 lastPlacedIndex = oldIndex也就是1

2 oldIndex为2 oldIndex > lastPlacedIndex 不动 lastPlacedIndex = oldIndex也就是2

3 oldIndex为3 oldIndex > lastPlacedIndex 不动 lastPlacedIndex = oldIndex也就是3

0 oldIndex为0 oldIndex < lastPlacedIndex 标记移动 lastPlacedIndex 不变还是3

4 oldIndex为4 oldIndex > lastPlacedIndex 不动 lastPlacedIndex 改为4