React源码解析之FunctionComponent(上)
- 2019 年 12 月 2 日
- 筆記
前言
在 React源码解析之workLoop 中讲到当workInProgress.tag
为FunctionComponent
时,会进行FunctionComponent
的更新:
//FunctionComponent的更新 case FunctionComponent: { //React 组件的类型,FunctionComponent的类型是 function,ClassComponent的类型是 class const Component = workInProgress.type; //下次渲染待更新的 props const unresolvedProps = workInProgress.pendingProps; // pendingProps const resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps); //更新 FunctionComponent //可以看到大部分是workInProgress的属性 //之所以定义变量再传进去,是为了“冻结”workInProgress的属性,防止在 function 里会改变workInProgress的属性 return updateFunctionComponent( //workInProgress.alternate current, workInProgress, //workInProgress.type Component, //workInProgress.pendingProps resolvedProps, renderExpirationTime, ); }
本文就来分析FunctionComponent
是如何更新的
一、updateFunctionComponent
作用: 执行FunctionComponent
的更新
源码:
//更新 functionComponent //current:workInProgress.alternate //Component:workInProgress.type //resolvedProps:workInProgress.pendingProps function updateFunctionComponent( current, workInProgress, Component, nextProps: any, renderExpirationTime, ) { //删掉了 dev 代码 //后面讲 context 的时候再作说明 const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); const context = getMaskedContext(workInProgress, unmaskedContext); let nextChildren; //做update 标记可不看 prepareToReadContext(workInProgress, renderExpirationTime); prepareToReadEventComponents(workInProgress); //删掉了 dev 代码 //在渲染的过程中,对里面用到的 hook函数做一些操作 nextChildren = renderWithHooks( current, workInProgress, Component, nextProps, context, renderExpirationTime, ); //如果不是第一次渲染,并且没有接收到更新的话 //didReceiveUpdate:更新上的优化 if (current !== null && !didReceiveUpdate) { bailoutHooks(current, workInProgress, renderExpirationTime); return bailoutOnAlreadyFinishedWork( current, workInProgress, renderExpirationTime, ); } // React DevTools reads this flag. //表明当前组件在渲染的过程中有被更新到 workInProgress.effectTag |= PerformedWork; //将 ReactElement 变成 fiber对象,并更新,生成对应 DOM 的实例,并挂载到真正的 DOM 节点上 reconcileChildren( current, workInProgress, nextChildren, renderExpirationTime, ); return workInProgress.child; }
解析: (1) 在「前言」的代码里也可以看到,传入updateFunctionComponent
的大部分参数都是workInProgress
这个 fiber 对象的属性
我在看这段的时候,忽然冒出一个疑问,为什么不直接传一个workInProgress
对象呢? 我自己的猜测是在外面「冻结」这些属性,防止在updateFunctionComponent()
中,修改这些属性
(2) 在updateFunctionComponent()
中,主要是执行了两个函数: ① renderWithHooks() ② reconcileChildren()
执行完这两个方法后,最终返回workInProgress.child
,即正在执行更新的 fiber 对象的第一个子节点
(3) bailoutOnAlreadyFinishedWork()
在 React源码解析之workLoop 中已经解析过,其作用是 跳过该节点及该节点上所有子节点的更新
(4) bailoutHooks()
的源码不多,作用是 跳过 hooks 函数的更新:
//跳过hooks更新 export function bailoutHooks( current: Fiber, workInProgress: Fiber, expirationTime: ExpirationTime, ) { workInProgress.updateQueue = current.updateQueue; workInProgress.effectTag &= ~(PassiveEffect | UpdateEffect); //置为NoWork 不更新 if (current.expirationTime <= expirationTime) { current.expirationTime = NoWork; } }
二、renderWithHooks
作用: 在渲染的过程中,对里面用到的 hooks 函数做一些操作
源码:
//渲染的过程中,对里面用到的 hook函数做一些操作 export function renderWithHooks( current: Fiber | null, workInProgress: Fiber, Component: any, props: any, refOrContext: any, nextRenderExpirationTime: ExpirationTime, ): any { renderExpirationTime = nextRenderExpirationTime; //当前正要渲染的 fiber 对象 currentlyRenderingFiber = workInProgress; //第一次的 state 状态 nextCurrentHook = current !== null ? current.memoizedState : null; //删除了 dev 代码 // The following should have already been reset // currentHook = null; // workInProgressHook = null; // remainingExpirationTime = NoWork; // componentUpdateQueue = null; // didScheduleRenderPhaseUpdate = false; // renderPhaseUpdates = null; // numberOfReRenders = 0; // sideEffectTag = 0; // TODO Warn if no hooks are used at all during mount, then some are used during update. // Currently we will identify the update render as a mount because nextCurrentHook === null. // This is tricky because it's valid for certain types of components (e.g. React.lazy) // Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used. // Non-stateful hooks (e.g. context) don't get added to memoizedState, // so nextCurrentHook would be null during updates and mounts. //删除了 dev 代码 //第一次渲染调用HooksDispatcherOnMount //多次渲染调用HooksDispatcherOnUpdate //用来存放 useState、useEffect 等 hook 函数的对象 ReactCurrentDispatcher.current = nextCurrentHook === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; //workInProgress.type,这里能当做 function 使用,说明 type 是 function let children = Component(props, refOrContext); //判断在执行 render的过程中是否有预定的更新 //当有更新要渲染时 if (didScheduleRenderPhaseUpdate) { do { //置为 false 说明该循环只会执行一次 didScheduleRenderPhaseUpdate = false; //重新渲染时fiber 的节点数 numberOfReRenders += 1; // Start over from the beginning of the list //记录 state,以便重新执行这个 FunctionComponent 内部的几个 useState 函数 nextCurrentHook = current !== null ? current.memoizedState : null; nextWorkInProgressHook = firstWorkInProgressHook; //释放当前 state currentHook = null; workInProgressHook = null; componentUpdateQueue = null; if (__DEV__) { // Also validate hook order for cascading updates. hookTypesUpdateIndexDev = -1; } //HooksDispatcherOnUpdate ReactCurrentDispatcher.current = __DEV__ ? HooksDispatcherOnUpdateInDEV : HooksDispatcherOnUpdate; children = Component(props, refOrContext); } while (didScheduleRenderPhaseUpdate); renderPhaseUpdates = null; numberOfReRenders = 0; } // We can assume the previous dispatcher is always this one, since we set it // at the beginning of the render phase and there's no re-entrancy. ReactCurrentDispatcher.current = ContextOnlyDispatcher; //定义新的 fiber 对象 const renderedWork: Fiber = (currentlyRenderingFiber: any); //为属性赋值 renderedWork.memoizedState = firstWorkInProgressHook; renderedWork.expirationTime = remainingExpirationTime; renderedWork.updateQueue = (componentUpdateQueue: any); renderedWork.effectTag |= sideEffectTag; if (__DEV__) { renderedWork._debugHookTypes = hookTypesDev; } // This check uses currentHook so that it works the same in DEV and prod bundles. // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles. const didRenderTooFewHooks = currentHook !== null && currentHook.next !== null; //重置 renderExpirationTime = NoWork; currentlyRenderingFiber = null; currentHook = null; nextCurrentHook = null; firstWorkInProgressHook = null; workInProgressHook = null; nextWorkInProgressHook = null; if (__DEV__) { currentHookNameInDev = null; hookTypesDev = null; hookTypesUpdateIndexDev = -1; } remainingExpirationTime = NoWork; componentUpdateQueue = null; sideEffectTag = 0; // These were reset above // didScheduleRenderPhaseUpdate = false; // renderPhaseUpdates = null; // numberOfReRenders = 0; invariant( !didRenderTooFewHooks, 'Rendered fewer hooks than expected. This may be caused by an accidental ' + 'early return statement.', ); return children; }
解析: 在开发者使用FunctionComponent
来写 React 组件的时候,是不能用setState
的,取而代之的是useState()
、useEffect
等 Hook API
所以在更新FunctionComponent
的时候,会先执行renderWithHooks()
方法,来处理这些 hooks
(1) nextCurrentHook 是根据current
来赋值的,所以 nextCurrentHook 也可以用来判断是否是 组件第一次渲染
(2) 无论是HooksDispatcherOnMount
还是HooksDispatcherOnUpdate
,它们都是 存放 useState、useEffect 等 hook 函数的对象:
const HooksDispatcherOnMount: Dispatcher = { readContext, useCallback: mountCallback, useContext: readContext, useEffect: mountEffect, useImperativeHandle: mountImperativeHandle, useLayoutEffect: mountLayoutEffect, useMemo: mountMemo, useReducer: mountReducer, useRef: mountRef, useState: mountState, useDebugValue: mountDebugValue, useEvent: updateEventComponentInstance, }; const HooksDispatcherOnUpdate: Dispatcher = { readContext, useCallback: updateCallback, useContext: readContext, useEffect: updateEffect, useImperativeHandle: updateImperativeHandle, useLayoutEffect: updateLayoutEffect, useMemo: updateMemo, useReducer: updateReducer, useRef: updateRef, useState: updateState, useDebugValue: updateDebugValue, useEvent: updateEventComponentInstance, };
可以看到,每个 Hook API 都对应一个更新的方法,这些我们后面再细说
(3) let children = Component(props, refOrContext);
这行我其实没看懂,因为Component
是workInProgress.type
,它的值可以是function
或是class
,但我没想到可以当做方法去调用Component(props, refOrContext)
所以我现在暂时还不知道 children 到底是个啥,后面如果有新发现的话,会在「前言」中提到。
(4) 然后是当didScheduleRenderPhaseUpdate
为true
时,执行一个while循环
,在循环中,会保存 state 的状态,并重置 hook、组件更新队列为 null,最终再次执行Component(props, refOrContext)
,得出新的 children
didScheduleRenderPhaseUpdate:
// Whether an update was scheduled during the currently executing render pass. //判断在执行 render的过程中是否有预定的更新 let didScheduleRenderPhaseUpdate: boolean = false;
这个循环,我的一个疑惑是,while
中将didScheduleRenderPhaseUpdate
置为false
,那么这个循环只会执行一次,为什么要用while
? 为什么没用if...else
?
暂时也是没有答案
(5) 定义新的 fiber 对象来保留操作 hooks 后得到的一些变量,最后再将有关 hooks 的变量都置为 null,return children
三、reconcileChildren
作用: 将 ReactElement 变成fiber
对象,并更新,生成对应 DOM 的实例,并挂载到真正的 DOM 节点上
源码:
export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderExpirationTime: ExpirationTime, ) { if (current === null) { // If this is a fresh new component that hasn't been rendered yet, we // won't update its child set by applying minimal side-effects. Instead, // we will add them all to the child before it gets rendered. That means // we can optimize this reconciliation pass by not tracking side-effects. //因为是第一次渲染,所以不存在current.child,所以第二个参数传的 null //React第一次渲染的顺序是先父节点,再是子节点 workInProgress.child = mountChildFibers( workInProgress, null, nextChildren, renderExpirationTime, ); } else { // If the current child is the same as the work in progress, it means that // we haven't yet started any work on these children. Therefore, we use // the clone algorithm to create a copy of all the current children. // If we had any progressed work already, that is invalid at this point so // let's throw it out. workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderExpirationTime, ); } }
解析: mountChildFibers()
和reconcileChildFibers()
调用的是同一个函数ChildReconciler
:
//true false export const reconcileChildFibers = ChildReconciler(true); export const mountChildFibers = ChildReconciler(false);
false 表示是第一次渲染,true 反之
四、ChildReconciler
作用: 同reconcileChildren()
这个方法有 1100 多行,前面全是 function 的定义,最后返回reconcileChildFibers
,所以我们从后往前看
源码:
//是否跟踪副作用 function ChildReconciler(shouldTrackSideEffects) { xxx xxx xxx function reconcileChildFibers(): Fiber | null { } return reconcileChildFibers; }
解析: 第一次渲染时无副作用(sideEffect)的,所以shouldTrackSideEffects=false
,多次渲染是有副作用的,所以shouldTrackSideEffects=true
这个方法太长了,先看最后 return 的reconcileChildFibers
五、reconcileChildFibers
作用: 针对不同类型的节点,进行不同的节点操作
源码:
// This API will tag the children with the side-effect of the reconciliation // itself. They will be added to the side-effect list as we pass through the // children and the parent. function reconcileChildFibers( returnFiber: Fiber, currentFirstChild: Fiber | null, //新计算出来的 children newChild: any, expirationTime: ExpirationTime, ): 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 && //在开发中写<div>{ arr.map((a,b)=>xxx) }</div>,这种节点称为 REACT_FRAGMENT_TYPE newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null; //type 为REACT_FRAGMENT_TYPE是不需要任何更新的,直接渲染子节点即可 if (isUnkeyedTopLevelFragment) { newChild = newChild.props.children; } // Handle object types const isObject = typeof newChild === 'object' && newChild !== null; //element 节点 if (isObject) { switch (newChild.$$typeof) { // ReactElement节点 case REACT_ELEMENT_TYPE: return placeSingleChild( reconcileSingleElement( returnFiber, currentFirstChild, newChild, expirationTime, ), ); //ReactDOM.createPortal(child, container) //https://zh-hans.reactjs.org/docs/react-dom.html#createportal case REACT_PORTAL_TYPE: return placeSingleChild( reconcileSinglePortal( returnFiber, currentFirstChild, newChild, expirationTime, ), ); } } //文本节点 if (typeof newChild === 'string' || typeof newChild === 'number') { return placeSingleChild( reconcileSingleTextNode( returnFiber, currentFirstChild, '' + newChild, expirationTime, ), ); } //数组节点 if (isArray(newChild)) { return reconcileChildrenArray( returnFiber, currentFirstChild, newChild, expirationTime, ); } //IteratorFunction if (getIteratorFn(newChild)) { return reconcileChildrenIterator( returnFiber, currentFirstChild, newChild, expirationTime, ); } //如果未符合上述的 element 节点的要求,则报错 if (isObject) { throwOnInvalidObjectType(returnFiber, newChild); } //删除了 dev 代码 //报出警告,可不看 if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) { // If the new child is undefined, and the return fiber is a composite // component, throw an error. If Fiber return types are disabled, // we already threw above. //即workInProgress,正在更新的节点 switch (returnFiber.tag) { case ClassComponent: { //删除了 dev 代码 } // Intentionally fall through to the next case, which handles both // functions and classes // eslint-disable-next-lined no-fallthrough case FunctionComponent: { const Component = returnFiber.type; invariant( false, '%s(...): Nothing was returned from render. This usually means a ' + 'return statement is missing. Or, to render nothing, ' + 'return null.', Component.displayName || Component.name || 'Component', ); } } } // Remaining cases are all treated as empty. //如果旧节点存在,但是更新的节点是 null 的话,需要删除旧节点的内容 return deleteRemainingChildren(returnFiber, currentFirstChild); }
解析: ① isUnkeyedTopLevelFragment 当我们在开发中写了 如
<div>{ arr.map((a,b)=>xxx) }</div>
的代码的时候,这种节点类型会被判定为REACT_FRAGMENT_TYPE
,React 会直接渲染它的子节点:
newChild = newChild.props.children;
② 如果 element type 是 object 的话,也就是ClassComponent
或FunctionComponent
会有两种情况: 一个是REACT_ELEMENT_TYPE
,即我们常见的 ReactElement 节点; 另一个是REACT_PORTAL_TYPE
,portal 节点,通常被应用于 对话框、悬浮卡、提示框上,具体请参考官方文档:Portals(https://zh-hans.reactjs.org/docs/portals.html)
REACT_ELEMENT_TYPE 的话,会执行reconcileSingleElement
方法
③ 如果是文本节点的话,会执行reconcileSingleTextNode
方法
④ 如果执行到最后的deleteRemainingChildren
话,说明待更新的节点是 null,需要删除原有旧节点的内容
可以看到ChildReconciler
中的reconcileChildFibers
方法的作用就是根据新节点newChild
的节点类型,来执行不同的操作节点函数
下篇文章,会讲reconcileSingleElement
、reconcileSingleTextNode
和deleteRemainingChildren
GitHub
ReactFiberBeginWork:https://github.com/AttackXiaoJinJin/reactExplain/react16.8.6/packages/react-reconciler/src/ReactFiberBeginWork.js
ReactFiberHooks:https://github.com/AttackXiaoJinJin/reactExplain/react16.8.6/packages/react-reconciler/src/ReactFiberHooks.js
ReactChildFiber:https://github.com/AttackXiaoJinJin/reactExplain/react16.8.6/packages/react-reconciler/src/ReactChildFiber.js