Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

diff #8

Open
bibi7 opened this issue Dec 15, 2019 · 1 comment
Open

diff #8

bibi7 opened this issue Dec 15, 2019 · 1 comment

Comments

@bibi7
Copy link
Owner

bibi7 commented Dec 15, 2019

function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    expirationTime: ExpirationTime,
  ): Fiber | null {
    // This algorithm can't optimize by searching from both ends since we
    // don't have backpointers on fibers. I'm trying to see how far we can get
    // with that model. If it ends up not being worth the tradeoffs, we can
    // add it later.

    // Even with a two ended optimization, we'd want to optimize for the case
    // where there are few changes and brute force the comparison instead of
    // going for the Map. It'd like to explore hitting that path first in
    // forward-only mode and only go for the Map once we notice that we need
    // lots of look ahead. This doesn't handle reversal as well as two ended
    // search but that's unusual. Besides, for the two ended optimization to
    // work on Iterables, we'd need to copy the whole set.

    // In this first iteration, we'll just live with hitting the bad case
    // (adding everything to a Map) in for every insert/move.

    // If you change this code, also update reconcileChildrenIterator() which
    // uses the same algorithm.

    if (__DEV__) {
      // First, validate keys.
      let knownKeys = null;
      for (let i = 0; i < newChildren.length; i++) {
        const child = newChildren[i];
        knownKeys = warnOnInvalidKey(child, knownKeys);
      }
    }

    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++) {
      // 第一个条件没看懂,想不到什么情况下会老的 fiber 的 index > newIdx
      //  正常来说 nextOldFiber 就是下一个节点了
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      // 如果 key 相同的话就可以复用 fiber。另外 oldFiber 如果为空的话,就会重新创建一个 fiber
      // 这个情况对应上面我看不懂的条件
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        expirationTime,
      );
      // 如果不能复用 fiber 话,就跳出循环
      if (newFiber === null) { 
        // TODO: This breaks on empty slots like null children. That's
        // unfortunate because it triggers the slow path all the time. We need
        // a better way to communicate whether this was a miss or null,
        // boolean, undefined, etc.
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      // 接下来都是可以复用 fiber 的逻辑
      // shouldTrackSideEffects 代表更新组件
      // 如果需要追踪副作用并且是重新创建了一个 fiber 的情况
      // 那么会把 oldFiber 删掉
      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) {
        // 第一次渲染赋值
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {
        // TODO: Defer siblings if we're not at the right index for this slot.
        // I.e. if we had null values before, then we want to defer this
        // for each null value. However, we also don't want to call updateSlot
        // with the previous one.
        // 链表连接新的 fiber 节点
        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],
          expirationTime,
        );
        if (!newFiber) {
          continue;
        }
        // 下面的逻辑和之前一样
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          // 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.
    // 这里指新老的子节点都没有遍历完,那就把老的子节点的 key 或者 index 生成一个 Map
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // Keep scanning and use the map to restore deleted items as moves.
    // 继续遍历新的子节点
    for (; newIdx < newChildren.length; newIdx++) {
      // 从 Map 中找出可以复用的 fiber 节点,不能复用就重新创建新的
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        expirationTime,
      );
      // 以下逻辑和之前的一样
      if (newFiber) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            // The new fiber is a work in progress, but if there exists a
            // current, that means that we reused the fiber. We need to delete
            // it from the child list so that we don't add it to the deletion
            // list.
            // 如果复用 fiber,就把原先的 fiber 删了
            existingChildren.delete(
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); // 2
        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;
  }
@bibi7
Copy link
Owner Author

bibi7 commented Dec 15, 2019

在这个diff函数中,旧的children是一个链表,新children是一个数组。for循环会开始同时对比新旧两个children。

第一个for循环

专门const一个变量newFiber,主要是用updateSlot函数来表达当前的child是否可以复用,可以点进去看看具体实现。

  function updateSlot(
    returnFiber: Fiber,
    oldFiber: Fiber | null,
    newChild: any,
    expirationTime: ExpirationTime,
  ): Fiber | null {
    // Update the fiber if the keys match, otherwise return null.

    const key = oldFiber !== null ? oldFiber.key : null;

    if (typeof newChild === 'string' || typeof newChild === 'number') {
      // Text nodes don't have keys. If the previous node is implicitly keyed
      // we can continue to replace it without aborting even if it is not a text
      // node.
      if (key !== null) {
        return null;
      }
      return updateTextNode(
        returnFiber,
        oldFiber,
        '' + newChild,
        expirationTime,
      );
    }
    if (typeof newChild === 'object' && newChild !== null) {
    //....
    }
    //....
}

函数挺长的,就不全部贴了,其实很轻松能通过最前面的注释了解到用途,主要是通过key来比对是否可以复用。

Update the fiber if the keys match, otherwise return null.

如果一旦第一个for循环中的child不可以复用的话,直接打断for循环。也不管后续的遍历。(其实这里我比较疑惑的是万一后续的可以复用呢?)
然后会继续判断一下当前newChild是否全部遍历完了?已经遍历完了的话就可以把老的全部删除就可以了。并且return结果的第一个fiber。

在这里第一轮遍历的结果有这么几个情况

  1. newChild全部遍历完毕了
  2. 老节点全部遍历完毕了

第二次for循环

新节点还没遍历完全,且老节点已经一滴也没有了,那么进入第二次for循环。直接把剩余新节点遍历,创建一个个新的fiber然后插入就行,最终也是return出一个结果。

第三次for循环

如果上面两个return的判断都走不进去,那么就是第三次遍历循环。核心逻辑是找出可以复用的老节点并移动位置。
首先我们会把所有剩余的老节点都丢到一个 map 中。
比如代码剩余的老节点为:

<p key={1}>1</p>
<p key={2}>2</p>

那么这个 map 的结构就会是这样的:

// 节点的 key 作为 map 的 key
// 如果节点不存在 key,那么 index 为 key
const map = {
    1: {},
    2: {}
}

在遍历的过程中会寻找新的节点的 key 是否存在于这个 map 中,存在即可复用,不存在就只能创建一个新的了。
那么如果复用成功,就应该把复用的 key 从 map 中删掉,并且给复用的节点移动位置。这里的移动依旧不涉及 DOM 操作,而是给 effectTag 赋值为 Placement。

此轮遍历结束后,就把还存在于 map 中的所有老节点删除。

总结下:

  1. 第一遍历新数组,新老数组相同 index 进行对比,通过 updateSlot方法找到可以复用的节点,直到找到不可以复用的节点就退出循环。
  2. 第一遍历完之后,删除剩余的老节点,追加剩余的新节点的过程。如果是新节点已遍历完成,就将剩余的老节点批量删除;如果是老节点遍历完成仍有新节点剩余,则将新节点直接插入。
  3. 把所有老数组元素按 key 或 index 放 Map 里,然后遍历新数组,插入老数组的元素,这是移动的情况。

其实这个和15版的diff最终体现其实都是那么几个步骤:插入,删除,移动。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant