react 源码中 的 diff 以及 对 key 的使用

无论是在 vue 还是 react 中,使用 key 都是一个 必须面对的问题,最近就对 react 的key 做了一点学习,这里就当做个人的学习笔记了

我们就来看这里的 (已删除 多余的代码,只留下了 数组类型的 子节点 )

// This API will tag the children with the side-effect of the reconciliation
  // itself. They will be added to the side-effect list as we pass through the
  // children and the parent.
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null, // 子节点的Fiber
    newChild: any,                   // 子节点的ReactElement
    expirationTime: ExpirationTime,  // fiber 循环中的 过期时间
  ): Fiber | null {
    // This function is not recursive.
    // If the top level item is an array, we treat it as a set of children,
    // not as a fragment. Nested arrays on the other hand will be treated as
    // fragment nodes. Recursion happens at the normal flow.

    // Handle top level unkeyed fragments as if they were arrays.
    // This leads to an ambiguity between <>{[...]}</> and <>...</>.
    // We treat the ambiguous cases above the same.
    
    // 这里 就是 处理 <></> 这样的子节点的,因为 这样的 节点是 没有 key 的,就把内部的 
    // 子节点 统一处理 为 数组子节点 
    const isUnkeyedTopLevelFragment =
      typeof newChild === 'object' &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children;
    }

    // Handle object types
    const isObject = typeof newChild === 'object' && newChild !== null;
    ...

    // 这里就是 当子节点 是数组的情况
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        expirationTime,
      );
    }
    ...
    
    // Remaining cases are all treated as empty.
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

接下来再 看 看看 reconcileChildrenArray

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

    let resultingFirstChild: Fiber | null = null;
    // 这里的 节点 是一个 链表的形式,只不过 next 变成了 sibling
    let previousNewFiber: Fiber | null = null;

    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    // 以相同的顺序 去遍历 新老节点,然后判断他的key 是否相同
    // 如果遇到 第一个 不相同的,则 立刻 跳出循环
    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],
        expirationTime,
      );
      // 当没有复用节点的情况下(也就是 key 不相同) 直接跳出循环
      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;
      }
      // shouldTrackSideEffects 这个参数 先放一边,是主函数 传进来使用的
      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.
        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);
      // 返回第一个子节点
      // 因为这里是一个 链表,只要知道 head 就可以知道所有的节点了
      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.
    // 创建一个 es6 中的 Map,并以 key 作为键,这个函数 后续会讲到
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // Keep scanning and use the map to restore deleted items as moves.
    for (; newIdx < newChildren.length; newIdx++) {
      // 先 看 是否是 number string 节点,是的话,直接去找 index(没有 key
      // 然后根据 key 或者 index 去获取节点 (这里的 existingChildren 如前文所讲,就是一个 map)
      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.
            // 如果复用了这个节点,那么 就 把 这个节点 从Map 里面删掉
            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

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) {
      // 根据 各个 $$type 进行判断(这里 每种 不同的 react 节点 都有 不同的 $$type ,默认是 symbol 类型)
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE: {
          // 只有当 key 相等的情况下,才会复用 这个节点
          // 可以看到,之前 中如果没有 key 的话,就是 null,而 null === null
          // 所以 如果没有写 key ,就会 默认复用 之前的节点
          if (newChild.key === key) {
            if (newChild.type === REACT_FRAGMENT_TYPE) {
              return updateFragment(
                returnFiber,
                oldFiber,
                newChild.props.children,
                expirationTime,
                key,
              );
            }
            return updateElement(
              returnFiber,
              oldFiber,
              newChild,
              expirationTime,
            );
          } else {
            // 否则返回 null 表示 不能复用当前节点,key 不相等,就放弃
            return null;
          }
        }
        case REACT_PORTAL_TYPE: {
          if (newChild.key === key) {
            return updatePortal(
              returnFiber,
              oldFiber,
              newChild,
              expirationTime,
            );
          } else {
            return null;
          }
        }
      }

      if (isArray(newChild) || getIteratorFn(newChild)) {
        if (key !== null) {
          return null;
        }

        return updateFragment(
          returnFiber,
          oldFiber,
          newChild,
          expirationTime,
          null,
        );
      }

      throwOnInvalidObjectType(returnFiber, newChild);
    }

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

    return null;
  }

再来看一下 mapRemainingChildren 函数

// 这个函数其实很简单  
function mapRemainingChildren(
    returnFiber: Fiber,
    currentFirstChild: Fiber,
  ): Map<string | number, Fiber> {
    // Add the remaining children to a temporary map so that we can find them by
    // keys quickly. Implicit (null) keys get added to this set with their index
    // instead.
    
    const existingChildren: Map<string | number, Fiber> = new Map();

    let existingChild = currentFirstChild;
    // 创建一个 map 对象,并以 key 或者 index 作为 键
    while (existingChild !== null) {
      if (existingChild.key !== null) {
        existingChildren.set(existingChild.key, existingChild);
      } else {
        existingChildren.set(existingChild.index, existingChild);
      }
      existingChild = existingChild.sibling;
    }
    return existingChildren;
  }

到这里了,就可以粗略的总结一下 key 发挥的作用了

  1. 先是 遍历一遍 新老节点,如果相同的话,直接复用,如果 都没有写key,默认 key 是一样的(都是 null),接着判断 $$type 是否相等
  2. 遇到第一个不相同的,直接 break循环
  3. 比较 当前 新老节点 遍历的长度,分别做处理
  4. 然后 根据 老节点 创建 一个 key | index 作为 key 的 map,所以说如果你使用 index 作为 key 的话,其实没有任何 用处,默认就是这样的
  5. 然后 新节点 使用 key 在 创建的 map 中取,如果有对应的可复用的节点,就复用
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值