前言:
在 React源碼解析之workLoop 中講到當workInProgress.tag
爲FunctionComponent
時,會進行FunctionComponent
的更新:javascript
//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
是如何更新的html
1、updateFunctionComponent
做用:
執行FunctionComponent
的更新java
源碼:node
//更新 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 對象的屬性react
我在看這段的時候,突然冒出一個疑問,爲何不直接傳一個workInProgress
對象呢?
我本身的猜想是在外面「凍結」這些屬性,防止在updateFunctionComponent()
中,修改這些屬性git
(2) 在updateFunctionComponent()
中,主要是執行了兩個函數:
① renderWithHooks()
② reconcileChildren()github
執行完這兩個方法後,最終返回workInProgress.child
,即正在執行更新的 fiber 對象的第一個子節點web
(3) bailoutOnAlreadyFinishedWork()
在 React源碼解析之workLoop 中已經解析過,其做用是 跳過該節點及該節點上全部子節點的更新數組
(4) bailoutHooks()
的源碼很少,做用是 跳過 hooks 函數的更新:app
//跳過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;
}
}
複製代碼
2、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
3、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 反之
4、ChildReconciler
做用:
同reconcileChildren()
這個方法有 1100 多行,前面全是 function 的定義,最後返回reconcileChildFibers
,因此咱們從後往前看
源碼:
//是否跟蹤反作用
function ChildReconciler(shouldTrackSideEffects) {
xxx
xxx
xxx
function reconcileChildFibers(): Fiber | null {
}
return reconcileChildFibers;
}
複製代碼
解析:
第一次渲染時無反作用(sideEffect)的,因此shouldTrackSideEffects=false
,屢次渲染是有反作用的,因此shouldTrackSideEffects=true
這個方法太長了,先看最後 return 的reconcileChildFibers
5、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
(完)