本文深入解析React Fiber架构中completeWork阶段的源码实现。重点阐述深度优先遍历下的DOM节点创建流程,包括createInstance与appendAllChildren的工作机制,以及真实DOM与Fiber对象的关联方式。同时详细剖析useEffect等副作用函数的Flags标记逻辑、Hook数据结构的构建,以及Effect环状链表的形成过程,为理解React渲染机制提供底层视角。
引言
前文已介绍beginWork阶段,React采用深度优先遍历算法构建整个Fiber树。
这意味着,并非所有节点的beginWork执行完毕后,才会统一进入completeWork阶段。
当某个节点的beginWork执行完毕且其next指针为null时,流程将转入completeWork。
一. completeUnitOfWork
function completeUnitOfWork(unitOfWork) {
var completedWork = unitOfWork;
do {
// ...
next = completeWork(current, completedWork, subtreeRenderLanes);
// ...
if (returnFiber !== null && (returnFiber.flags & Incomplete) === NoFlags) {
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
var flags = completedWork.flags;
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
}else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
var siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
// ...
}
依据深度优先算法,当beginWork处理完某棵子树的最后一个节点后,即进入completeUnitOfWork。该函数以当前节点为起点执行completeWork,随后逐层向上回溯,直至找到具有兄弟节点的父级节点。
核心逻辑包含两部分:一是执行completeWork,二是完成Effect的挂载,并将所有节点(含内部节点)的Effect串联成环状链表。
二. completeWork
function completeWork(current, workInProgress, renderLanes) {
var newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent:
// ...
case HostRoot:
// ...
updateHostContainer(workInProgress);
return null;
case HostComponent:
// ...
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
// ...
// ...
}
值得注意的是,React内部对HostContainer的update操作目前为空函数,未来版本可能会补充具体逻辑。普通组件节点则会进入HostComponent分支处理。
createInstance
function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
var parentNamespace;
{
// TODO: take namespace into account when validating.
var hostContextDev = hostContext;
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
if (typeof props.children === 'string' || typeof props.children === 'number') {
var string = '' + props.children;
var ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type);
validateDOMNesting(null, string, ownAncestorInfo);
}
parentNamespace = hostContextDev.namespace;
}
var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
return domElement;
}
createElement
function createElement(type, props, rootContainerElement, parentNamespace) {
// script标签处理...
// ...
// flow相关webComponents处理
// ...
domElement = ownerDocument.createElement(type);
// select标签特殊处理,单独设置multiple以及size
// 非法标签告警处理...
return domElement;
}
至此,代码开始为每个Fiber节点创建对应的DOM对象,但此时并未将其插入文档流。那么,真实DOM是如何与Fiber对象建立关联的呢?
precacheFiberNode函数会在每个真实DOM节点下挂载对应的Fiber节点,通过添加以_reactFiber$加随机数命名的属性来实现。
updateFiberProps函数则会在每个真实DOM节点下挂载对应的props及children(即Element对象),通过添加以_reactProps$加随机数命名的属性来实现。
这里产生一个疑问:每个Fiber对应的真实DOM对象是单个构建并存储,还是构建一棵完整的DOM树?二者之间是如何具体关联的?
appendAllChildren
appendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) {
var node = workInProgress.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
} else if (node.tag === HostPortal) ; else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}
逻辑已十分清晰:根据前文创建的Fiber链表,遍历node节点。对于普通节点,将调用appendInitialChild,即执行parentInstance.appendChild(child)。循环结束后,便生成了整个待插入的DOM节点结构(在页面首次渲染时)。
另外需注意,Fiber与DOM的关联正是在此阶段完成的(注意是DOM关联Fiber,而非Fiber关联DOM)。
若存在ref,当前Fiber树的flags将与Ref的二进制数据进行位运算或操作(这一机制至关重要)。
三. Effect
React推崇函数式编程,若函数组件中包含useEffect等方法,React将其视为包含副作用的函数组件。那么,这些副作用是如何组织起来的?又在何时运行?
在beginWork阶段,Fiber的flags默认均为二进制0。若存在副作用(如useEffect、ref、useLayoutEffect等),首次将被标记为Placement。但为何包含useEffect的函数组件Fiber对象,其flags数值往往大于256?
我们以useEffect为例,深入探究其原理。
useEffect
function useEffect(create, deps) {
var dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
没错,useEffect方法的定义仅包含两行代码。
resolveDispatcher
function resolveDispatcher() {
var dispatcher = ReactCurrentDispatcher.current;
if (!(dispatcher !== null)) {
{
throw Error( "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem." );
}
}
return dispatcher;
}
我们继续查看ReactCurrentDispatcher的定义:
const ReactCurrentDispatcher = {
/**
* @internal
* @type {ReactComponent}
*/
current: (null: null | Dispatcher),
};
最初的ReactCurrentDispatcher在renderRoot阶段初始化。若root或Lane发生变化,原有的dispatch可能被置空或首次不存在,此时将使用ContextOnlyDispatcher作为替代。
在函数组件beginWork阶段,执行函数组件生成Element对象之前,会赋值HooksDispatcherOnMount,这便是dispatch。
我们来看看HooksDispatcher:
{
readContext: function (context, observedBits) {
return readContext(context, observedBits);
},
useCallback: function (callback, deps) {
currentHookNameInDev = 'useCallback';
mountHookTypesDev();
checkDepsAreArrayDev(deps);
return mountCallback(callback, deps);
},
useContext: function (context, observedBits) {
currentHookNameInDev = 'useContext';
mountHookTypesDev();
return readContext(context, observedBits);
},
useEffect: function (create, deps) {
currentHookNameInDev = 'useEffect';
mountHookTypesDev();
checkDepsAreArrayDev(deps);
return mountEffect(create, deps);
},
useImperativeHandle: function (ref, create, deps) {
currentHookNameInDev = 'useImperativeHandle';
mountHookTypesDev();
checkDepsAreArrayDev(deps);
return mountImperativeHandle(ref, create, deps);
},
useLayoutEffect: function (create, deps) {
currentHookNameInDev = 'useLayoutEffect';
mountHookTypesDev();
checkDepsAreArrayDev(deps);
return mountLayoutEffect(create, deps);
},
useMemo: function (create, deps) {
currentHookNameInDev = 'useMemo';
mountHookTypesDev();
checkDepsAreArrayDev(deps);
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountMemo(create, deps);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
},
useReducer: function (reducer, initialArg, init) {
currentHookNameInDev = 'useReducer';
mountHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountReducer(reducer, initialArg, init);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
},
useRef: function (initialValue) {
currentHookNameInDev = 'useRef';
mountHookTypesDev();
return mountRef(initialValue);
},
useState: function (initialState) {
currentHookNameInDev = 'useState';
mountHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
},
useDebugValue: function (value, formatterFn) {
currentHookNameInDev = 'useDebugValue';
mountHookTypesDev();
return mountDebugValue();
},
useDeferredValue: function (value) {
currentHookNameInDev = 'useDeferredValue';
mountHookTypesDev();
return mountDeferredValue(value);
},
useTransition: function () {
currentHookNameInDev = 'useTransition';
mountHookTypesDev();
return mountTransition();
},
useMutableSource: function (source, getSnapshot, subscribe) {
currentHookNameInDev = 'useMutableSource';
mountHookTypesDev();
return mountMutableSource(source, getSnapshot, subscribe);
},
useOpaqueIdentifier: function () {
currentHookNameInDev = 'useOpaqueIdentifier';
mountHookTypesDev();
return mountOpaqueIdentifier();
},
unstable_isNewReconciler: enableNewReconciler
};
其中useEffect重点执行mountEffect(create, deps)。
function mountEffect(create, deps) {
{
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if ('undefined' !== typeof jest) {
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber$1);
}
}
return mountEffectImpl(Update | Passive, Passive$1, create, deps);
}
mountEffectImpl
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber$1.flags |= fiberFlags;
hook.memoizedState = pushEffect(HasEffect | hookFlags, create, undefined, nextDeps);
}
Hook对象的数据结构如下:
{
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
这里产生几个问题:
- currentlyRenderingFiber是什么?它与workInProgressFiber有何关系?
- 多个函数组件与单个函数组件中的多个Hook是如何关联起来的?
- 整个Hook与Fiber是如何关联起来的?
- 完整的Hook数据结构又是什么?
currentlyRenderingFiber指当前正在rendering阶段的Fiber对象,该对象在renderHook初始化阶段已被赋值为workInProgressFiber。因此,当前函数组件的flags在此处被修改,即包含副作用的函数flags = flags | Update | Passive。
根据二进制位运算,根函数组件库的flags等于518。当然,这个数值并非固定不变,因为beginWork阶段初始的flags值会随不同的effects而变化。
pushEffect
function pushEffect(tag, create, destroy, deps) {
var effect = {
tag: tag,
create: create,
destroy: destroy,
deps: deps,
// Circular
next: null
};
var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
对于tag的值,是HasEffect和Passive按位运算或的结果,实际上固定为5。
需要注意的是,函数组件的updateQueue与rootFiber不同,普通节点的数据结构和作用也各异。函数组件的updateQueue关联的是effect,这与render初始化阶段rootFiber存在巨大差异。
上述代码逻辑简单:若函数组件存在多个effect,会将这些effect顺序关联起来。该函数组件的fiber.updateQueue对应lastEffect,next指向下一个effect,直至形成首尾相连的环状链表结构。
为何设计为环状?这将在后续的调度阶段得到解释。
再思考一个问题:此处仅解决了单个组件内effect的构建,那么整个Fiber链表中的effect是如何构建的?执行顺序又是怎样的?
四. rootFiber-Effect
在completeWork最后阶段,依据深度优先遍历算法,将每个节点的firstEffect层层向上传递,直至rootFiber。lastEffect同样层层向上判断,取上层最后一个effect作为rootFiber的lastEffect。
每个Fiber的effect通过nextEffect链接起来,而Fiber内部则通过updateQueue链接自身的effect环状链表。
至此,completeWork阶段宣告完成,rootFiber及各Fiber节点的大部分属性均已构建完毕。
下一章,将进入commit阶段,更多关于React Fiber构建completeWork的资料请关注本站其它相关文章!
以上就是React Fiber构建completeWork源码解析的详细内容,更多关于React Fiber构建completeWork的资料请关注本站其它相关文章!