Reconclier

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

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

ChildReconciler

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

//ReactFiber.jsexport 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~