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雷同可能复用旧节点,然而如果元素类型不一样不会复用旧节点
- 首先会进行第一次的循环,它做的事件就是更新节点。
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掉循环,不解决以后节点,因为之后的程序可能曾经乱掉了,所以之后的节点会在解决挪动节点的时候再进行解决。
当循环完最初一次的新节点时候,能够删除其余多余不须要的旧节点
- 第二次循环是在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~