初窥React-13 (render-3 differ算法)
先转一张图,介绍differ算法:
differ遵循的原则:
-
只对同级比较,跨层级的dom不会进行复用
-
不同类型节点生成的dom树不同,此时会直接销毁老节点及子孙节点,并新建节点
-
可以通过key来对元素diff的过程提供复用的线索
reconcileChildFibers这个方法会根据newChild的类型来进入单节点的diff或者多节点diff:
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) { // 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. var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null; if (isUnkeyedTopLevelFragment) { newChild = newChild.props.children; } // Handle object types var isObject = typeof newChild === 'object' && newChild !== null; if (isObject) { 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: { var payload = newChild._payload; var init = newChild._init; // TODO: This function is supposed to be non-recursive. return reconcileChildFibers(returnFiber, currentFirstChild, init(payload), lanes); } } } if (typeof newChild === 'string' || typeof newChild === 'number') { return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes)); } //多节点differ if (isArray$1(newChild)) { return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes); } if (getIteratorFn(newChild)) { return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, lanes); } if (isObject) { throwOnInvalidObjectType(returnFiber, newChild); } { if (typeof newChild === 'function') { warnOnFunctionType(returnFiber); } } if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) { // If the new child is undefined, and the return fiber is a composite // component, throw an error. If Fiber return types are disabled, // we already threw above. switch (returnFiber.tag) { case ClassComponent: { { var instance = returnFiber.stateNode; if (instance.render._isMockFunction) { // We allow auto-mocks to proceed as if they're returning null. break; } } } // Intentionally fall through to the next case, which handles both // functions and classes // eslint-disable-next-lined no-fallthrough case Block: case FunctionComponent: case ForwardRef: case SimpleMemoComponent: { { { throw Error( (getComponentName(returnFiber.type) || 'Component') + "(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null." ); } } } } } // Remaining cases are all treated as empty. return deleteRemainingChildren(returnFiber, currentFirstChild); }
单一节点相关的:
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) { var key = element.key; var 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 (child.tag) { case Fragment: { if (element.type === REACT_FRAGMENT_TYPE) { deleteRemainingChildren(returnFiber, child.sibling); var existing = useFiber(child, element.props.children); existing.return = returnFiber; { existing._debugSource = element._source; existing._debugOwner = element._owner; } return existing; } break; } case Block: { var type = element.type; if (type.$$typeof === REACT_LAZY_TYPE) { type = resolveLazyType(type); } if (type.$$typeof === REACT_BLOCK_TYPE) { // The new Block might not be initialized yet. We need to initialize // it in case initializing it turns out it would match. if (type._render === child.type._render) { deleteRemainingChildren(returnFiber, child.sibling); var _existing2 = useFiber(child, element.props); _existing2.type = type; _existing2.return = returnFiber; { _existing2._debugSource = element._source; _existing2._debugOwner = element._owner; } return _existing2; } } } // We intentionally fallthrough here if enableBlocksAPI is not on. // eslint-disable-next-lined no-fallthrough default: { if (child.elementType === element.type || ( // Keep this check inline so it only runs on the false path: isCompatibleFamilyForHotReloading(child, element) )) { deleteRemainingChildren(returnFiber, child.sibling); var _existing3 = useFiber(child, element.props); _existing3.ref = coerceRef(returnFiber, child, element); _existing3.return = returnFiber; { _existing3._debugSource = element._source; _existing3._debugOwner = element._owner; } return _existing3; } break; } } // Didn't match. // key相同type不同,标记删除兄弟 deleteRemainingChildren(returnFiber, child); break; } else { //key不同直接删除该节点 deleteChild(returnFiber, child); } child = child.sibling; } //新建新fiber if (element.type === REACT_FRAGMENT_TYPE) { var created = createFiberFromFragment(element.props.children, returnFiber.mode, lanes, element.key); created.return = returnFiber; return created; } else { var _created4 = createFiberFromElement(element, returnFiber.mode, lanes); _created4.ref = coerceRef(returnFiber, currentFirstChild, element); _created4.return = returnFiber; return _created4; } }
在源码中多节点diff有三个for循环遍历(并不意味着所有更新都有经历三个遍历,进入循环体有条件,也有条件跳出循环),第一个遍历处理节点的更新(包括props更新和type更新和删除),第二个遍历处理其他的情况(节点新增),其原因在于在大多数的应用中,节点更新的频率更加频繁,第三个处理位节点置改变 (转)
-
第一次遍历 因为老的节点存在于current Fiber中,所以它是个链表结构,还记得Fiber双缓存结构嘛,节点通过child、return、sibling连接,而newChildren存在于jsx当中,所以遍历对比的时候,首先让newChildren[i]
与
oldFiber对比,然后让i++、nextOldFiber = oldFiber.sibling。在第一轮遍历中,会处理三种情况,其中第1,2两种情况会结束第一次循环- key不同,第一次循环结束
- newChildren或者oldFiber遍历完,第一次循环结束
- key同type不同,标记oldFiber为DELETION
- key相同type相同则可以复用
newChildren遍历完,oldFiber没遍历完,在第一次遍历完成之后将oldFiber中没遍历完的节点标记为DELETION,即删除的DELETION Tag
-
第二个遍历 第二个遍历考虑三种情况
-
newChildren和oldFiber都遍历完:多节点diff过程结束
-
newChildren没遍历完,oldFiber遍历完,将剩下的newChildren的节点标记为Placement,即插入的Tag
-
newChildren和oldFiber没遍历完,则进入节点移动的逻辑
-
-
第三个遍历 主要逻辑在placeChild函数中,例如更新前节点顺序是ABCD,更新后是ACDB
-
newChild中第一个位置的A和oldFiber第一个位置的A,key相同可复用,lastPlacedIndex=0
-
newChild中第二个位置的C和oldFiber第二个位置的B,key不同跳出第一次循环,将oldFiber中的BCD保存在map中
-
newChild中第二个位置的C在oldFiber中的index=2 > lastPlacedIndex=0不需要移动,lastPlacedIndex=2
-
newChild中第三个位置的D在oldFiber中的index=3 > lastPlacedIndex=2不需要移动,lastPlacedIndex=3
-
newChild中第四个位置的B在oldFiber中的index=1 < lastPlacedIndex=3,移动到最后
-
多节点相关的:
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) { // 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. { // First, validate keys. var knownKeys = null; for (var i = 0; i < newChildren.length; i++) { var child = newChildren[i]; knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber); } } var resultingFirstChild = null; var previousNewFiber = null; var oldFiber = currentFirstChild; var lastPlacedIndex = 0; var newIdx = 0; var nextOldFiber = null; // 第一次遍历 因为老的节点存在于current Fiber中,所以它是个链表结构,还记得Fiber双缓存结构嘛,节点通过child、return、sibling连接,而newChildren存在于jsx当中,所以遍历对比的时候,首先让newChildren[i]与oldFiber对比,然后让i++、nextOldFiber = oldFiber.sibling。在第一轮遍历中,会处理三种情况,其中第1,2两种情况会结束第一次循环 // 1.key不同,第一次循环结束 // 2.newChildren或者oldFiber遍历完,第一次循环结束 // 3.key同type不同,标记oldFiber为DELETION // 4.key相同type相同则可以复用 // newChildren遍历完,oldFiber没遍历完,在第一次遍历完成之后将oldFiber中没遍历完的节点标记为DELETION,即删除的DELETION Tag for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { //老的child index> index,则说明位置不匹配 if (oldFiber.index > newIdx) { nextOldFiber = oldFiber; oldFiber = null; } else { nextOldFiber = oldFiber.sibling; } var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes); 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; } 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); return resultingFirstChild; } // 第二个遍历 第二个遍历考虑三种情况 // 1.newChildren和oldFiber都遍历完:多节点diff过程结束 // 2.newChildren没遍历完,oldFiber遍历完,将剩下的newChildren的节点标记为Placement,即插入的Tag // 3.newChildren和oldFiber没遍历完,则进入节点移动的逻辑 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++) { var _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; } return resultingFirstChild; } // Add all children to a key map for quick lookups. var existingChildren = mapRemainingChildren(returnFiber, oldFiber); // Keep scanning and use the map to restore deleted items as moves. // 第三个遍历 主要逻辑在placeChild函数中,例如更新前节点顺序是ABCD,更新后是ACDB // 1.newChild中第一个位置的A和oldFiber第一个位置的A,key相同可复用,lastPlacedIndex=0 // 2.newChild中第二个位置的C和oldFiber第二个位置的B,key不同跳出第一次循环,将oldFiber中的BCD保存在map中 // 3.newChild中第二个位置的C在oldFiber中的index=2 > lastPlacedIndex=0不需要移动,lastPlacedIndex=2 // 4.newChild中第三个位置的D在oldFiber中的index=3 > lastPlacedIndex=2不需要移动,lastPlacedIndex=3 // 5.newChild中第四个位置的B在oldFiber中的index=1 < lastPlacedIndex=3,移动到最后 for (; newIdx < newChildren.length; newIdx++) { var _newFiber2 = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes); if (_newFiber2 !== null) { if (shouldTrackSideEffects) { if (_newFiber2.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(_newFiber2.key === null ? newIdx : _newFiber2.key); } } lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx); if (previousNewFiber === null) { resultingFirstChild = _newFiber2; } else { previousNewFiber.sibling = _newFiber2; } previousNewFiber = _newFiber2; } } if (shouldTrackSideEffects) { // Any existing children that weren't consumed above were deleted. We need // to add them to the deletion list. existingChildren.forEach(function (child) { return deleteChild(returnFiber, child); }); } return resultingFirstChild; }