React源码解析之FunctionComponent(上)

前言:
在 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
REACT_ELEMENT_TYPE 的话,会执行reconcileSingleElement
方法
③ 如果是文本节点的话,会执行reconcileSingleTextNode
方法
④ 如果执行到最后的deleteRemainingChildren
话,说明待更新的节点是 null,需要删除原有旧节点的内容
可以看到ChildReconciler
中的reconcileChildFibers
方法的作用就是根据新节点newChild
的节点类型,来执行不同的操作节点函数
下篇文章,会讲reconcileSingleElement
、reconcileSingleTextNode
和deleteRemainingChildren
GitHub:
ReactFiberBeginWork

(完)