讲解完函数组件和类组件是如何计算状态更新之后,这篇文章讲一下reconcile
的流程,也就是我们俗称的diff
算法。
类组件的diff
入口在finishClassComponent
中
function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes) {
markRef(current, workInProgress);
var didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;
if (!shouldUpdate && !didCaptureError) {
// 根据shouldComponentUpdate生命周期决定是否需要更新组件
if (hasContext) {
invalidateContextProvider(workInProgress, Component, false);
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
var instance = workInProgress.stateNode;
ReactCurrentOwner$1.current = workInProgress;
var nextChildren;
if (didCaptureError && typeof Component.getDerivedStateFromError !== 'function') {
// 出现错误
nextChildren = null;
{
stopProfilerTimerIfRunning();
}
} else {
{
setIsRendering(true);
// 执行render方法
nextChildren = instance.render();
if ( workInProgress.mode & StrictMode) {
// 严格模式
disableLogs();
try {
instance.render();
} finally {
reenableLogs();
}
}
setIsRendering(false);
}
}
workInProgress.flags |= PerformedWork;
if (current !== null && didCaptureError) {
forceUnmountCurrentAndReconcile(current, workInProgress, nextChildren, renderLanes);
} else {
// diff的入口
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
workInProgress.memoizedState = instance.state;
if (hasContext) {
invalidateContextProvider(workInProgress, Component, true);
}
return workInProgress.child;
}
对于函数组件,会在updateFunctionComponent
中,renderWithHooks
之后,调用reconcileChildren
进入diff
入口函数
reconcileChildren
方法定义如下:
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
}
}
当组件是初次加载时,会执行mountChildFibers
方法,更新时执行reconcileChildFibers
,这两个方法定义如下:
var reconcileChildFibers = ChildReconciler(true);
var mountChildFibers = ChildReconciler(false);
再看ChildReconciler
function ChildReconciler(shouldTrackSideEffects) {
// ...
return reconcileChildFibers
}
shouldTrackSideEffects
表示是否有副作用。当组件初次挂载时,显然是没有副作用的,而组件更新可能会涉及到元素的删除,插入等操作,因此shouldTrackSideEffects
为true
。接下来看这个方法的返回值:reconcileChildFibers
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
var isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
}
// ...
}
// ...
if (isArray$1(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
}
// ...
}
这个方法会根据不同的节点类型来进行对应的diff
操作:比如对于单个react元素,会执行reconcileSingleElement
,对于多个元素,执行reconcileChildrenArray
。这也就是单节点diff
和多节点diff
,下面就会分析这两种算法流程。再开始分析之前,先看一下reconcileChildFibers
的参数:
单节点diff
单节点diff
是指新的节点为单个节点时的diff
流程。单节点diff
由三种可能的情况:
- 老
fiber
为空 - 老
fiber
有一个节点 - 老
fiber
有多个节点
单节点diff
比较简单,只需要在老fiber
中找到key
和type
与新的jsx
节点都相同节点,然后删除剩余老节点即可。如果找不到,删除所有老节点,创建新的节点。
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
var key = element.key;
var child = currentFirstChild;
// 循环currentFirstChild这一层的所有老fiber节点
while (child !== null) {
if (child.key === key) {
switch (child.tag) {
// ...
default:
{
if (child.elementType === element.type || (
isCompatibleFamilyForHotReloading(child, element) )) {
// 因为是单节点diff,所以找到key和type均相同的节点后,直接删除所有剩余节点即可
deleteRemainingChildren(returnFiber, child.sibling);
// 根据老fiber创建新fiber
var _existing3 = useFiber(child, element.props);
_existing3.ref = coerceRef(returnFiber, child, element);
_existing3.return = returnFiber;
// 。。。
// 当找到key和type均相同的节点时,直接return新fiber
return _existing3;
}
break;
}
}
// key相同,但是type不同
deleteRemainingChildren(returnFiber, child);
break;
} else {
// key不同,直接删除遍历到的老fiber
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 当没有找到key和type均相同的节点时,根据jsx创建新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;
}
}
这里需要注意几个点:
-
在执行
deleteChild
(deleteRemainingChildren
内也会调用deleteChild
)时会为要删除的就fiber
打上Deletion
的tag
,表示这个旧的节点要被删除(注意,并不是真的删除这个节点,而是打上tag
)。 -
如果要被删除的节点还有子节点,只会在要被删除的节点上打上
tag
,不会在其子节点上打tag
-
当找到可复用的
fiber
节点时(key和type相同),会创建一个新的fiber
节点,并建立新的fiber
节点和旧的fiber
节点之间的联系,即设置alternate
属性。但是当不能复用时,新旧fiber
之间的alternate
连接是不存在的。 -
细心的同学可能发现了,既然旧
fiber
会被打上Deletion
的tag,那么新fiber
节点呢?注意reconcileChildFibers
有这样的代码:return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
在
placeSingleChild
方法中才会对新增的节点打上Placement
的tag
多节点diff
单节点diff看完后,来看一下多节点diff。回到reconcileChildFibers
方法,这里会做一个特殊处理
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;
// 如果新的jsx节点是没有key的Fragment节点,则取出它的children
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// 多节点diff
if (isArray$1(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
}
}
可以看到,多节点diff就是新的jsx
存在多个节点时的diff策略。
这里首先说明一下,react
的diff
策略并不是不惜代价地复用节点,而是在保证效率的基础上进行复用。比如一个组件发生了跨层级的移动,虽然只是位置上的变化,但是react
则不会复用这个节点,这一点很多文章里也讲过,就不展开了。下面讲解一下react
的多节点diff
策略。
首先是几个变量
var resultingFirstChild = null; // 新fiber链表中的第一个fiber节点
var newIdx = 0; // 用于循环新jsx数组中的指针
var previousNewFiber = null; // 新fiber链表中,当前fiber的前一个fiber
var oldFiber = currentFirstChild; // 用于循环旧fiber节点的指针
var nextOldFiber = null; // 老fiber链表中,当前fiber的下一个fiber
// 老fiber树中最靠右的一个不需要移动的fiber节点,在老fiber树中的位置,下文会讲到
var lastPlacedIndex = 0;
第一部分
首先看多节点diff的第一部分:针对节点更新的循环
// 循环新fiber和老fiber
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// 更新fiber节点
// 如果新的jsx返回null,或者新老fiber的key不同,updateSlot返回null
// 如果新老fiber的key相同,但是type不同,或者老fiber不存在,说明fiber不能复用(注意前文提到的fiber复用的含义)
// 如果新老fiber都存在,并且能够复用,则复用fiber
var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
// newFiber为null时,直接跳出循环
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
// 组件更新,shouldTrackSideEffects为true
// 组件挂载,shouldTrackSideEffects为false,前文有提到
if (shouldTrackSideEffects) {
// 如果没有发生fiber复用,说明老fiber被删除
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
// 确定新fiber的位置,placeChild方法下文会单独讲
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 记录新fiber中的第一个节点
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
可以看出,只有在新的jsx
返回null
,或者新老fiber
的key
不同时,才会中途跳出循环。如果中途跳出了循环,会跳过下文的第二和第三部分,直接进入第四部分
第二部分
如果循环正常结束,没有中途跳出,会进入第二部分:
// 如果新fiber遍历完毕,直接删除旧fiber中的剩余节点即可,并返回resultingFirstChild
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
第三部分
接下来是第三部分:
// 如果老fiber遍历结束,则剩余的新fiber都是新增节点,直接新增即可
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
var _newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (_newFiber === null) {
continue;
}
// 放置新增节点
lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = _newFiber;
} else {
previousNewFiber.sibling = _newFiber;
}
previousNewFiber = _newFiber;
}
return resultingFirstChild;
}
这里需要注意,如果第一部分的循环中途退出,则新旧fiber
都不会遍历完毕,因此是不会进入第二和第三部分的,而是会直接进入第四部分。
第四部分
// 首先将老fiber链表中没有遍历到的剩余节点放到一个map中,key是fiber的key或者在老fiber链表中的位置索引,value是fiber节点
var existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 循环新的jsx数组中的剩余部分
for (; newIdx < newChildren.length; newIdx++) {
// 如果新的jsx返回null,updateFromMap返回null,跳过本轮循环
// 新jsx不返回null,从existingChildren中找到与新jsx的key相同的老fiber,看是否能够复用fiber
var _newFiber2 = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);
if (_newFiber2 !== null) {
if (shouldTrackSideEffects) {
if (_newFiber2.alternate !== null) {
// 当新fiber非空,并且新fiber复用了老fiber,说明新老fiber存在对应关系,从existingChildren中删除老fiber
existingChildren.delete(_newFiber2.key === null ? newIdx : _newFiber2.key);
}
}
// 放置新fiber
lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = _newFiber2;
} else {
previousNewFiber.sibling = _newFiber2;
}
previousNewFiber = _newFiber2;
}
}
第五部分
最后还有一个收尾工作,遍历existingChildren
,删除掉其中的旧fiber
节点,并返回新fiber
链表的第一个节点
if (shouldTrackSideEffects) {
existingChildren.forEach(function (child) {
return deleteChild(returnFiber, child);
});
}
return resultingFirstChild;
这样,beginWork
就会拿到这个函数的返回值,并返回到performUnitOfWork
中,用来修改全局变量workInProgress
,从而继续执行workLoopSync
循环。
placeChild方法
placeChild
是用来确定新fiber
节点在新fiber
链表中的位置,并返回前文中提到的lastPlacedIndex
。接下来看一下代码
function placeChild(newFiber, lastPlacedIndex, newIndex) {
// 确定新fiber节点的位置索引
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
// 不采取任何操作
return lastPlacedIndex;
}
var current = newFiber.alternate;
if (current !== null) {
// current不为null,说明新老fiber有关联
var oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// 老fiber在lastPlacedIndex左边,无需更新lastPlacedIndex
newFiber.flags = Placement;
return lastPlacedIndex;
} else {
// 老fiber在lastPlacedIndex的右边,说明在新fiber链表中,对应节点发生了移动
return oldIndex;
}
} else {
// current为null,说明新老fiber没有关联,直接插入新fiber
newFiber.flags = Placement;
return lastPlacedIndex;
}
}
下面举个例子:
旧fiber链表
A -> B -> C -> D -> E
新fiber链表
A -> B -> D -> E -> C
- 当
newIndex
为0,进入第一部分的循环,执行到placeChild
方法,由于新老fiber
节点存在关联,因此current
不为空,而oldIndex
和lastPlacedIndex
都是0,因此返回了oldIndex
- 当
newIndex
为1,和第一步流程相同,也返回了oldIndex(1)
,lastPlacedIndex
变为1 - 当
newIndex
为2,跳出第一部分的循环,进入第四部分,执行到placeChild
方法,newFiber
为D节点,current
为老fiber
链表的D节点,因此current
不为空,oldIndex
为3,lastPlacedIndex
为1,因此返回3,lastPlacedIndex
变为3 - 当
newIndex
为3,和第3步流程相同,lastPlacedIndex
变为4 - 当
newIndex
为4,执行到placeChild
方法,newFiber
为C节点,current
为老fiber
链表的C节点,current
不为空,oldIndex
为2,lastPlacedIndex
为4,此时oldIndex
小于lastPlacedIndex
,因此react
认为C节点发生了移动,为其打上Placement的tag
因此,lastPlacedIndex
的含义就是:在老fiber
链表中,最靠右的一个不需要移动的fiber
节点,在老fiber
链表中的位置索引。
最后再来个整体的流程图吧
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!