React Fiber构建completeWork源码解析
作者:GW_刘振
引言
之前我们介绍了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,依次往上,直到找到相邻节点。
核心方法分为2部分,其一:completeWork,其二:完成effect挂载,并串联所有节点的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内部updateHostContainer居然是个空函数,也许后续版本做do something吧。 普通节点将进入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,precache是以_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)
最后如何存在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怎么关联起来的?
- 完整的hooks数据结构又是什么?
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顺序关联起来,这个函数组件的fiberr.updateQueue对应lastEffect,next即下一个effect,直到最后形成一个首尾相连的环状链表结构。
为什么是环状?这个待到后续调度阶段再解释。
再思考一个问题:这里只是解决了单个组件内的effect构建,那么整个fiber链表里effect构建是怎么样的?执行的顺序又是什么?
四. rootFiber-Effect
在completedWork的最后,根据深度优先遍历算法,将每个节点的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的资料请关注脚本之家其它相关文章!