react基础(2)-key 到底是什么

136 阅读4分钟

魔幻的key

key在vue和react中都有用到,都是用来diff的,那么key到底是用来做什么的?

以下是vue官网中对于key的解释。

Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染

所以如果列表渲染中,每个列表项的dom都不大一样或者非常简单的时候,不用key可以减少dom的重建,但是如果列表项中包含子组件,子组件又依赖父组件,不添加key的情况,很容易造成列表变化没有引起diff重新渲染。

结论:key在react/vue中都是为了在diff的过程中进行优化的一种手段。只是使用的方式不同。

react diff 过程

react中的diff又叫reconcile.即协调

对不同的节点有不同的协调过程:

单节点

 function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
    // 从左到右的遍历
    while (child !== null) {
      // TODO: If key === null and child.key === null, then this only applies to
      // the first item in the list.
      if (child.key === key) {
       // switch case 
      } else {
        deleteChild(returnFiber, child);
      }
      // 下一个节点 
      child = child.sibling;
    }

    // 创建新Fiber,并返回

数组

 function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<any>,
    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;
    // old和new均没有到达尾部的时候
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
      // oldFiber比较完了,直接跳出循环
        nextOldFiber = oldFiber; 
        oldFiber = null;
      } else {
      // 指针向后走
        nextOldFiber = oldFiber.sibling;
      }
      // 更新fiber, updateSlot 在key不相等或不存在的时候会返回null,否则会返回一个fiber
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      // 无可复用的fibber(key不相等或者不存在),break循环
      if (newFiber === null) {
      // oldFiber比较完了
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      // 如果有sideEffect
      if (shouldTrackSideEffects) {
      // 旧的子元素 Fiber 链表中是否存在当前位置的 Fiber 对象,
      // 同时新的 Fiber 对象的 `alternate` 属性是否为 `null`。
      // 如果这两个条件都满足,说明当前位置的 Fiber 对象没有被重用,需要将其从 Fiber 树中删除。
        if (oldFiber && newFiber.alternate === null) {
          deleteChild(returnFiber, oldFiber);
        }
      }
      // 记录比较的位置
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
      // 新链表的头指针记录,只有第一个元素会走到这
        resultingFirstChild = newFiber;
      } else {
        // 新链表的头指针,指向下一个位置,新增
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    if (newIdx === newChildren.length) {
      // newArray已经走完了,删除fiber中剩余的元素
      deleteRemainingChildren(returnFiber, oldFiber);
      if (getIsHydrating()) {
        const numberOfForks = newIdx;
        pushTreeFork(returnFiber, numberOfForks);
      }
      return resultingFirstChild;
    }

    if (oldFiber === null) {
    // 如果oldFiber没走,则需要新增newArray中的数据到previousNewFiber
      
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) {
          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;
      }
      if (getIsHydrating()) {
        const numberOfForks = newIdx;
        pushTreeFork(returnFiber, numberOfForks);
      }
      return resultingFirstChild;
    }

    // Add all children to a key map for quick lookups.
    // newArray 和OldFiber都没走完,将fiber变成key-fiber的一个map
    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) {
            // 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.
            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));
    }

    if (getIsHydrating()) {
      const numberOfForks = newIdx;
      pushTreeFork(returnFiber, numberOfForks);
    }
    return resultingFirstChild;
  }

从上面的源码可以看出,在diff的过程中,发生了两次遍历,第一次是将newArray和oldFiber进行比较,在比较的过程中同时会生成一个newFiber:

  1. oldFiber与newArray比较,如果oldFiber或者Array都已经比遍历结束,则跳出循环。
  2. oldFiber和newArray的当前值比较,如果是key相同,type相同,则用新的array的props更新新的fiber
  3. key相同,type不同,删除旧的,新增新的
  4. key type都不同,跳出循环

第二次比较:

  1. 只剩下旧的节点(删除旧节点)
  2. 只剩下新的节点(新增新节点)
  3. 都有剩下则进行下一步的处理:
  • 将旧的fiber用一个key-fiber的map存起来
  • 遍历新的数组,如果在旧的fiber中找到fiber,则利用旧的fiber并更新并删除key,否则新建
  • 删除map中剩余的key和fiber.(多余的)

比较react和Vue

相同点:

没有key会默认使用index作为key,则发生元素删除活添加的时候,会重新渲染整个列表,而不是只渲染删除或者增加的元素

不同点:

react中的链表是单链表,有一个指针指向兄弟节点,一个指针指向父亲节点,diff的过程只能单向遍历。

vue中的链表是双向链表,所以可以进行首尾遍历,进行优化。