「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。
react 版本:v17.0.3
在React的diff过程中,无论是对单节点的比较还是对多节点的比较,只要找到key和elementType相同的旧节点,就会通过useFiber() 函数复用旧节点。下面,我们来解读一下 useFiber 方法。
useFiber
// react-reconciler/src/ReactChildFiber.new.js
// 复用旧节点
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
// 基于旧fiber和新的props.children 克隆生成一个新的 fiber,从而复用旧fiber节点
const clone = createWorkInProgress(fiber, pendingProps);
clone.index = 0;
clone.sibling = null;
return clone;
}
可以看到,useFiber 做的事情十分简单,就是调用 createWorkInProgress() 函数,基于旧fiber节点和新内容的 props,克隆生成一个新的fiber节点,从而实现旧节点的复用。并在返回新的fiber节点前将其 index属性重置为0,sibling属性重置为 null。
在调用 createWorkInProgress 克隆旧节点时,通过 双缓冲 的方式来完成旧节点的复用。下面我们来看看 createWorkInProgress 函数。
createWorkInProgress
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
// We use a double buffering pooling technique because we know that we'll
// only ever need at most two versions of a tree. We pool the "other" unused
// node that we're free to reuse. This is lazily created to avoid allocating
// extra objects for things that are never updated. It also allow us to
// reclaim the extra memory if needed.
// 使用 双缓冲池技术是因为 只需要最多两个版本的 fiber 树,就可以重用另一个版本的fiber树上的节点
// 因为一棵 fiber 树顶多有两个版本,所以当某一 fiber 节点不更新时,在更新 fiber 树的时候,
// 不会去重新创建跟之前一样的 fiber 节点,而是从另一个版本的 fiber 树上重用它
// 新建 fiber 节点
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
// 复用旧节点的属性
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
// ...
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
// Needed because Blocks store data on type.
// 复用旧节点的 type
workInProgress.type = current.type;
// We already have an alternate.
// Reset the effect tag.
workInProgress.flags = NoFlags;
// The effects are no longer valid.
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
if (enableProfilerTimer) {
// We intentionally reset, rather than copy, actualDuration & actualStartTime.
// This prevents time from endlessly accumulating in new commits.
// This has the downside of resetting values for different priority renders,
// But works for yielding (the common case) and should support resuming.
workInProgress.actualDuration = 0;
workInProgress.actualStartTime = -1;
}
}
// Reset all effects except static ones.
// Static effects are not specific to a render.
// 复用旧节点的属性
workInProgress.flags = current.flags & StaticMask;
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
// Clone the dependencies object. This is mutated during the render phase, so
// it cannot be shared with the current fiber.
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
// These will be overridden during the parent's reconciliation
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
if (enableProfilerTimer) {
workInProgress.selfBaseDuration = current.selfBaseDuration;
workInProgress.treeBaseDuration = current.treeBaseDuration;
}
// ...
return workInProgress;
}
在 React 中最多会同时存在两个 Fiber 树。当前屏幕上显示的内容对应的 Fiber 树称为 current Fiber 树,正在内存中构建的 Fiber 树称为 workInProgress Fiber 树。current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。
// current 为当前屏幕上显示的内容对应的 Fiber树
// workInProgress 为内存中正在构建的 Fiber树
workInProgress.alternate = current;
current.alternate = workInProgress;
在 createWorkInProgress 函数中,首先获取 current Fiber 树 的副本作为workInProgress Fiber 树:
let workInProgress = current.alternate;
如果 workInProgress 为 null,调用 createFiber() 函数,基于旧节点的 tag、key、mode属性和新内容的props,构建一个新的fiber节点,然后复用旧节点的其它属性,并将当前的 current Fiber 树 和 workInProgress Fiber 树通过 alternate 属性连接起来。
如果 workInProgress 不为 null,则直接将current 的副本作为workInProgress ,然后复用旧节点的属性。
在 createWorkInProgress 函数中,调用了 createFiber() 函数构建一个新的fiber节点,我们来看看 createFiber。
createFiber
// react-reconciler/src/ReactFiber.new.js
const createFiber = function (
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
// 返回 fiberNode 节点的实例
return new FiberNode(tag, pendingProps, key, mode);
};
在 createFiber 中,返回了FiberNode的一个实例对象,即返回一个新的fiber节点。下面贴出 FiberNode 构造函数的源码:
//react-reconciler/src/ReactFiber.new.js
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
if (enableProfilerTimer) {
// Note: The following is done to avoid a v8 performance cliff.
//
// Initializing the fields below to smis and later updating them with
// double values will cause Fibers to end up having separate shapes.
// This behavior/bug has something to do with Object.preventExtension().
// Fortunately this only impacts DEV builds.
// Unfortunately it makes React unusably slow for some applications.
// To work around this, initialize the fields below with doubles.
//
// Learn more about this here:
// https://github.com/facebook/react/issues/14365
// https://bugs.chromium.org/p/v8/issues/detail?id=8538
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN;
// It's okay to replace the initial doubles with smis after initialization.
// This won't trigger the performance cliff mentioned above,
// and it simplifies other profiler code (including DevTools).
this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}
// ...
}
总结
本文介绍了 useFiber() 这个函数,它的作用是克隆旧节点,从而复用旧节点。在克隆旧节点时,是通过 双缓冲 的方式来复用旧节点。