关于react.js:React-reconclier

58次阅读

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

Reconclier

React 应用虚构 dom 代替实在的 dom 节点,当数据被扭转的时候就须要用到算法来更新所有的旧节点。React 会通过 diff 算法比拟新旧节点并进行增删改。

在官网的文档里,解决 React diff 的动作叫做 协调(reconcile)。明天就讲一讲协调的代码。(代码版本为 v17.0.2)

ChildReconciler

ChildReconciler 是一个包装函数, 用于辨别 mount 和 diff 的

//ReactFiber.js
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

//ReactFiberBeginWork.js
 function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {if (current === null) {workInProgress.child = mountChildFibers(...);
  } else {workInProgress.child = reconcileChildFibers(...);
  }
}

因为初始挂载的时候只须要将子节点全副增加进去,并不需要 diff 算法,所以会用 shouldTrackSideEffects 变量辨别,当它为 false 的时候就示意为挂载函数。

ReconcileChildFibers

ChildReconciler 返回闭包内的一个函数 reconcileChildFibers

 function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): 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.
    const isUnkeyedTopLevelFragment =
      typeof newChild === 'object' &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
    if (isUnkeyedTopLevelFragment) {newChild = newChild.props.children;}

    // Handle object types
    if (typeof newChild === 'object' && newChild !== null) {switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_PORTAL_TYPE:
          return placeSingleChild(
            reconcileSinglePortal(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_LAZY_TYPE:
          if (enableLazyElements) {
            const payload = newChild._payload;
            const init = newChild._init;
            // TODO: This function is supposed to be non-recursive.
            return reconcileChildFibers(
              returnFiber,
              currentFirstChild,
              init(payload),
              lanes,
            );
          }
      }

      if (isArray(newChild)) {
        return reconcileChildrenArray(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }

      if (getIteratorFn(newChild)) {
        return reconcileChildrenIterator(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }

      throwOnInvalidObjectType(returnFiber, newChild);
    }

    if ((typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number'
    ) {
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '' + newChild,
          lanes,
        ),
      );
    }

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

    // Remaining cases are all treated as empty.
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

新节点会有几种状况

  • 当新节点为 Object 类型并且不等于 null,而且还要有 $$typeof 属性的时候阐明只有一个子节点,只须要依据不同的 React 元素类型去更新替换。
  • 当新节点为 Array 的时候,须要算法去比拟新旧节点,实现更新,删除,替换。
  • 当新节点对象含有 Iterator 迭代器的时候,须要进行其余解决,迭代器的反对个别用于不可变列表等
  • 当新节点是字符串且不为空的时候或者为数字,只须要当做文本替换

当子节点为单节点的时候,只须要调用 reconcileSingleElement 将节点进行更新

ReconcileChildrenArray

React 更新多节点的时候会用到算法

function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ): Fiber | null {if (__DEV__) {
      let knownKeys = null;
      for (let i = 0; i < newChildren.length; i++) {const child = newChildren[i];
        knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
      }
    }

    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) {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) {deleteRemainingChildren(returnFiber, oldFiber);
      if (getIsHydrating()) {
        const numberOfForks = newIdx;
        pushTreeFork(returnFiber, numberOfForks);
      }
      return resultingFirstChild;
    }

    if (oldFiber === null) {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;
    }

    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));
    }

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

React 跟 Vue 的 diff 有几个一样的机制(没有说抄 Vue, React 先出的 vdom🐶

  • 只比拟同层元素
  • 不同类型的节点比拟会创立新的节点和子节点,而后销毁旧节点,所以不会复用子节点
  • key 雷同可能复用旧节点,然而如果元素类型不一样不会复用旧节点
  1. 首先会进行第一次的循环,它做的事件就是更新节点。

updateSlot

新旧节点会带入 updateSlot 函数里进行更新

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

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

    if ((typeof newChild === 'string' && newChild !== '') ||
      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, lanes);
    }

    if (typeof newChild === 'object' && newChild !== null) {switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE: {if (newChild.key === key) {return updateElement(returnFiber, oldFiber, newChild, lanes);
          } else {return null;}
        }
        case REACT_PORTAL_TYPE: {if (newChild.key === key) {return updatePortal(returnFiber, oldFiber, newChild, lanes);
          } else {return null;}
        }
        case REACT_LAZY_TYPE: {if (enableLazyElements) {
            const payload = newChild._payload;
            const init = newChild._init;
            return updateSlot(returnFiber, oldFiber, init(payload), lanes);
          }
        }
      }

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

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

      throwOnInvalidObjectType(returnFiber, newChild);
    }

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

    return null;
  }

updateSlot对应不同的新节点类型进行不同的更新。对于更新单节点的时候会判断是否为同样的 key,并且在更新的时候判断类型是否统一抉择复用或者创立新的节点

当旧节点存在 key 且不匹配新节点 key 时,阐明可能被挪动到别处了,updateSlot此时会返回 null, 并且 break 掉循环, 不解决以后节点,因为之后的程序可能曾经乱掉了,所以之后的节点会在解决挪动节点的时候再进行解决。

当循环完最初一次的新节点时候,能够删除其余多余不须要的旧节点

  1. 第二次循环是在 oldFiber 等于 null 的时候,旧节点曾经到尾了,然而新节点还没循环完,阐明没有能够复用的旧节点,但存在新的节点。此时,须要增加新节点放在 newFiber 链表里。
 if (oldFiber === null) {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;
    }

3. 第三次循环就是解决挪动的节点。首先会创立一个 Map 对象去保留旧节点,可能不便疾速查找现有的节点。之后就能够通过查找新节点的 key 匹配旧节点的 key 去更新

    // 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) {
            // 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;
      }
    }

placeChild

节点是否挪动的逻辑在 placeChild 函数

 function placeChild(
    newFiber: Fiber,
    lastPlacedIndex: number,
    newIndex: number,
  ): number {
    newFiber.index = newIndex;
    if (!shouldTrackSideEffects) {
      newFiber.flags |= Forked;
      return lastPlacedIndex;
    }
    const current = newFiber.alternate;
    if (current !== null) {
      const oldIndex = current.index;
      if (oldIndex < lastPlacedIndex) {
        // This is a move.
        newFiber.flags |= Placement;
        return lastPlacedIndex;
      } else {
        // This item can stay in place.
        return oldIndex;
      }
    } else {
      // This is an insertion.
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    }
  }

如果旧节点的地位大于最初一次替换的地位则不须要挪动,否则挪动到最初头

1. 首先从节点 A 开始比照,新旧节点始终会进行更新,并且两个节点的 index 都为 0,所以oldFiber.index >= lastPlacedIndex, 不须要挪动

2. 新旧节点 key 不同,break 跳出循环, 将 BCD 放进 map 构造里。开始进行节点挪动,C 节点原地位为 2,是大于 lastplaceIndex = 0 的,所以不须要挪动,lastplaceIndex 更新为 2

3.D 节点原地位为 3,大于 lastplaceIndex, 不须要挪动,lastplaceIndex 更新为 3

4.B 节点的原地位为 2, 小于 lastplaceIndex,挪动到最初面,至此,所有的节点处理完毕

再看一个例子

1.A 和 D 节点 key 不雷同,break 第一次循环,将 ABCD 放进 map 构造里

2. 开始进行节点挪动,D 的原地位 oldFiber.index(3) >= lastPlacedIndex (0),不须要挪动。lastPlacedIndex 更新为 D 的原地位 3

3.A 节点原地位为 0,小于 lastplaceIndex,挪动到最初面,lastPlacedIndex 还是为 3

4.B 节点原地位为 1,小于 lastplaceIndex,挪动到最初面,lastPlacedIndex=3

5.C 节点原地位为 2,小于 lastplaceIndex,挪动到最初面,lastPlacedIndex=3。所有的节点处理完毕

总结

react 利用 key,只进行同级比照, 缩小比照工夫。然而 react 没有在 diff 里应用双端 (both end) 算法,双端算法可能缩小挪动节点的次数。因为 fiber 是一个单向链表,如果要用双端算法,须要所有的节点复制到一个汇合里或者减少反向指针。

码文不易,给个 star~

正文完
 0