关于前端:react-fiber-中的dom-diff

29次阅读

共计 6788 个字符,预计需要花费 17 分钟才能阅读完成。

两种节点类型

咱们能够从同级的节点数量将 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

正文完
 0