多種不同類型的組件的更新過程,以及如何遍歷節(jié)點(diǎn)形成新的 Fiber 樹,即 reconcilerChildren 調(diào)和子節(jié)點(diǎn)的過程。
-1. 入口和優(yōu)化
- 判斷組件更新是否可以優(yōu)化
- 根據(jù)節(jié)點(diǎn)類型分發(fā)處理
- 根據(jù) expirationTime 等信息判斷是否可以跳過
幫助優(yōu)化整個(gè)樹的更新過程的方法。
只有 ReactDOM.render() 的時(shí)才會更新 RootFiber,其后的更新都是在子節(jié)點(diǎn)上。
workLoop:
function workLoop(isYieldy) {
if (!isYieldy) {
// Flush work without yielding
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {
// Flush asynchronous work until the deadline runs out of time.
while (nextUnitOfWork !== null && !shouldYield()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
}
performUnitOfWork:更新子樹,調(diào)用了 beginWork:
function performUnitOfWork(workInProgress: Fiber): Fiber | null {
const current = workInProgress.alternate;
// See if beginning this work spawns more work.
startWorkTimer(workInProgress);
let next;
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
startProfilerTimer(workInProgress);
}
next = beginWork(current, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
if (workInProgress.mode & ProfileMode) {
// Record the render duration assuming we didn't bailout (or error).
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
}
} else {
// 這里返回子節(jié)點(diǎn)
next = beginWork(current, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
}
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
}
ReactCurrentOwner.current = null;
return next;
}
beginWork:
- 判斷如果是非首次渲染(current !== null):
新老 props 一樣,而且本次更新任務(wù)的優(yōu)先級并沒有超過現(xiàn)有任務(wù)的最高優(yōu)先級,則做一些優(yōu)化的工作,然后調(diào)用 xxx 用于跳過當(dāng)前 Fiber 樹及其子節(jié)點(diǎn)的所有更新。
- 然后可能是非首次但沒能跳過,也可能仍然是首次渲染(代碼太多,沒貼)。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
const updateExpirationTime = workInProgress.expirationTime;
// 傳入的 current,第一次渲染
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps === newProps &&
!hasLegacyContextChanged() &&
(updateExpirationTime === NoWork ||
updateExpirationTime > renderExpirationTime)
) {
// 處理不同類型的節(jié)點(diǎn)
// This fiber does not have any pending work. Bailout without entering
// the begin phase. There's still some bookkeeping we that needs to be done
// in this optimized path, mostly pushing stuff onto the stack.
switch (workInProgress.tag) {
case HostRoot:
// 太多,暫略
}
// 用于跳過子節(jié)點(diǎn)的更新
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
}
// 然后可能是非首次但沒能跳過,也可能仍然是首次渲染(代碼太多,沒貼)。
}
bailoutOnAlreadyFinishedWork:
用于跳過子節(jié)點(diǎn)的更新。
但也要看任務(wù)優(yōu)先級也不緊急的話,就函數(shù)返回 null,外部的 while 遍歷就停止了,也就跳過了所有子組件的更新。
但如果優(yōu)先級更高的話,則克隆 current 上面的 child 并返回,然后再返回到 workLoop 中,進(jìn)入下次 child 更新循環(huán),去嘗試更新子節(jié)點(diǎn)。這就是個(gè)不斷向下遍歷節(jié)點(diǎn)的過程。
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
cancelWorkTimer(workInProgress);
if (current !== null) {
// Reuse previous context list
workInProgress.firstContextDependency = current.firstContextDependency;
}
if (enableProfilerTimer) {
// Don't update "base" render times for bailouts.
stopProfilerTimerIfRunning(workInProgress);
}
// Check if the children have any pending work.
const childExpirationTime = workInProgress.childExpirationTime;
if (
childExpirationTime === NoWork ||
childExpirationTime > renderExpirationTime
) {
// The children don't have any work either. We can skip them.
// TODO: Once we add back resuming, we should check if the children are
// a work-in-progress set. If so, we need to transfer their effects.
return null;
} else {
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
}
0. 各種不同類型組件的更新
先說整體概念,既然是不同類型的組件更新,因此關(guān)注的粒度就是在一整棵 fiber 樹中,某一層是某一種類型的組件,其上的更新。而其子組件的更新,會在下一次 workLoop 遍歷的時(shí)候再真正處理。
接下來是各種組件類型的更新,也就是調(diào)和 Fiber 子節(jié)點(diǎn)的過程。
在 react-reconciler/ReactFiberBeginWork.js/beginWork() 方法中:
Fiber 上的 tag 標(biāo)記了不同的組件類型,在這里用作 switch 的判斷,根據(jù)不同組件類型分別進(jìn)行 fiber 的調(diào)和更新:
switch (workInProgress.tag) {
case IndeterminateComponent: {
const elementType = workInProgress.elementType;
return mountIndeterminateComponent(
current,
workInProgress,
elementType,
renderExpirationTime,
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
updateExpirationTime,
renderExpirationTime,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderExpirationTime);
case HostComponent:
return updateHostComponent(current, workInProgress, renderExpirationTime);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(
current,
workInProgress,
renderExpirationTime,
);
case HostPortal:
return updatePortalComponent(
current,
workInProgress,
renderExpirationTime,
);
case ForwardRef: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === type
? unresolvedProps
: resolveDefaultProps(type, unresolvedProps);
return updateForwardRef(
current,
workInProgress,
type,
resolvedProps,
renderExpirationTime,
);
}
case Fragment:
return updateFragment(current, workInProgress, renderExpirationTime);
case Mode:
return updateMode(current, workInProgress, renderExpirationTime);
case Profiler:
return updateProfiler(current, workInProgress, renderExpirationTime);
case ContextProvider:
return updateContextProvider(
current,
workInProgress,
renderExpirationTime,
);
case ContextConsumer:
return updateContextConsumer(
current,
workInProgress,
renderExpirationTime,
);
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps = resolveDefaultProps(type.type, unresolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
updateExpirationTime,
renderExpirationTime,
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
updateExpirationTime,
renderExpirationTime,
);
}
case IncompleteClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return mountIncompleteClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
default:
invariant(
false,
'Unknown unit of work tag. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
}
1. Function component 的更新
updateFunctionComponent:
之前說過每個(gè) Fiber 節(jié)點(diǎn)上的 type 就是指 createReactElement 時(shí)傳入第一個(gè)參數(shù),即 函數(shù)/class/原生dom標(biāo)簽字符串/內(nèi)置的某些類型(如React.Fragment 什么的,大多數(shù)時(shí)候會是個(gè) symbol 標(biāo)記)。
所以從 type 上獲取對應(yīng)的組件函數(shù),傳入 nextProps 和 context 執(zhí)行后獲取 nextChildren,也就是函數(shù)組件返回的東西,作為自己的 children。
但是 children 是 react element,因此需要還需要調(diào)用 reconcileChildren 涉及到 轉(zhuǎn)化為 Fiber 對象和更新等。
然后返回 workInProgress.child,因?yàn)閯偛?reconcileChildren 時(shí)會把處理好的 fiber 掛載到 child 上。
函數(shù)組件的更新如此看來是比較簡單的,主要復(fù)雜的地方在 reconcileChildren 的過程中。
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderExpirationTime,
) {
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
const context = getMaskedContext(workInProgress, unmaskedContext);
let nextChildren;
prepareToReadContext(workInProgress, renderExpirationTime);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
ReactCurrentFiber.setCurrentPhase('render');
nextChildren = Component(nextProps, context);
ReactCurrentFiber.setCurrentPhase(null);
} else {
// 這里調(diào)用函數(shù)組件,傳入props和context,等到該函數(shù)組件的子 element 樹。
nextChildren = Component(nextProps, context);
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
// 復(fù)雜的在這個(gè)方法中
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}
2. reconcileChildren
- 根據(jù) reactElement 上的 props.children 生成 fiber 子樹。
- 判斷 Fiber 對象是否可以復(fù)用。因?yàn)橹挥械谝淮问钦w全部渲染,而后續(xù)更新時(shí)自然要考慮復(fù)用。
- 列表根據(jù) key 優(yōu)化。
- 最終迭代處理完整個(gè) fiber 樹。
調(diào)和子節(jié)點(diǎn),主要分為第一次渲染,和后續(xù)更新。二者區(qū)別通過變量 shouldTrackSideEffects “是否追蹤副作用” 來區(qū)分,也就是非第一次渲染,會涉及到相關(guān)副作用的處理和復(fù)用。
reconcileChildren:
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderExpirationTime: ExpirationTime,
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderExpirationTime,
);
}
}
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
props.children 中的合法的成員主要就是 數(shù)組/字符串/數(shù)字 以及 react element。
- React.Fragment 的是臨時(shí)的節(jié)點(diǎn),渲染更新時(shí)要被跳過,newChild = newChild.props.children,也就是把下一層的 children 賦值為當(dāng)前某個(gè)待更新的組件的 children。
- 找到可復(fù)用的節(jié)點(diǎn)進(jìn)行 return,而不可復(fù)用的節(jié)點(diǎn),也就是 key 變了,就不會復(fù)用老的 fiber,老的 fiber 被刪除。就涉及到重新創(chuàng)建子節(jié)點(diǎn)。
而重新創(chuàng)建子節(jié)點(diǎn)時(shí)要看子節(jié)點(diǎn)的類型:
對于 REACT_ELEMENT_TYPE:
在 reconcileSingleElement 中根據(jù)不同的組件類型,得到不同的 fiberTag,然后調(diào)用 createFiber(fiberTag, pendingProps, key, mode) 創(chuàng)建創(chuàng)建不同的 Fiber。
對于 string 或者 number,也就是文本節(jié)點(diǎn):
只看第一個(gè)節(jié)點(diǎn)是不是文本節(jié)點(diǎn):
- 如果此前老的第一個(gè)子節(jié)點(diǎn)也是文本節(jié)點(diǎn),那么就復(fù)用留著,而刪除相鄰節(jié)點(diǎn),因?yàn)楝F(xiàn)在要更新為文本節(jié)點(diǎn)了,所以留一個(gè)節(jié)點(diǎn)就夠用了。
- 如果不是,那么就整個(gè)刪除老的子節(jié)點(diǎn)。
對于 Array 或者 IteratorFn(有迭代器的函數(shù)):下一節(jié)再說。
**
如果以上情況都不符合,那就全部當(dāng)做非法(我編的術(shù)語)子節(jié)點(diǎn),因此就將其全部“刪除”即可。
嘴上說著刪除,但實(shí)際上,不能真的刪,現(xiàn)在是在 workInProgress fiber 樹上進(jìn)行更新操作,并不會真的刪除 dom,而只是打相應(yīng)的標(biāo)記,是刪除操作?那就給 fiber 節(jié)點(diǎn)打上 Deletion 標(biāo)記,也就是:
childToDelete.effectTag = Deletion。
之前說過更新分兩個(gè)階段,render (有可能被打斷) 和 commit (不會被打斷) 階段,在 render 階段為這些 fiber 打上相應(yīng)的操作標(biāo)記后,在后面的 commit 階段在根據(jù)這些標(biāo)記,去真正的操作瀏覽器 dom。
3. key 和數(shù)組調(diào)和
- key 的作用。作為對比判斷依據(jù),從而盡量復(fù)用老的 fiber 節(jié)點(diǎn)。
- 對比數(shù)組 children 是否可復(fù)用。
- generator 和 Array 的區(qū)別,基本差不多,只是前者是 ES6 迭代器相關(guān)知識,需要不斷調(diào)用 next() 來獲取成員。
使用 react 時(shí)如果返回的是數(shù)組(如使用 Array.prototype.map),需要為每個(gè)子項(xiàng)指定 key 值。
**
以相同順序分別遍歷新老 children,對比 key 是否相同 來決定是否復(fù)用老的 fiber 節(jié)點(diǎn):
直到遇到 key 開始不相同了,就不再對標(biāo)著復(fù)用,而此時(shí) props.children 也就是 react element 的數(shù)組還有剩余,也就是還沒全部轉(zhuǎn)化為 fiber。那么有兩種情況:
- 對位的老的子節(jié)點(diǎn) oldFiber 已經(jīng)用完了,那么就為剩余未轉(zhuǎn)換的 react element 每個(gè)都單獨(dú)創(chuàng)建 fiber 對象。
- 如果 oldFiber 還有剩余,只是一一對位的 key 開始變得和新的 key 不匹配,所以才打斷了第一階段的復(fù)用。但其實(shí)還有機(jī)會進(jìn)行復(fù)用,可以遍歷剩余的 oldFiber,以其 key 作為 Map 數(shù)據(jù)結(jié)構(gòu)的 key,進(jìn)行存儲。然后看新的 key 是否能從 Map 中找到相應(yīng)的 oldFiber,以便進(jìn)行復(fù)用。這說明本次更新中,某個(gè)節(jié)點(diǎn)是位置只是位置順序變了。還是可以找到并復(fù)用的。Map 中剩余的就是真的沒用了,就標(biāo)記為刪除。
4. ClassComponent
在 react hooks 出現(xiàn)之前,唯一能引起二次更新的方法,就是 class 實(shí)例上的 setState 和 forceUpdate
- 計(jì)算新的 state:會使用 Object.assign({}, preState, particalState),用局部 state 對 preState 進(jìn)行淺覆蓋,來生成新的 state。
- 在 class 實(shí)例上,分別根據(jù)初次渲染還是后續(xù)更新來調(diào)用不同的生命周期方法。
5. IndeterminateComponent
在最初第一次渲染時(shí),對于所有的 functionalComponent 都初始標(biāo)記為 IndeterminateComponent 類型,
然后主要根據(jù)其返回的 value 中是否有 render 方法,從而才將 workInProgress.tag 其進(jìn)一步明確為 ClassComponent 還是 FunctionComponent。
基于內(nèi)部這種判斷邏輯,我們竟然可以通過在函數(shù)式組件中返回的對象上提供 render 函數(shù),以此將函數(shù)式組件“模擬”出了 class 組件的形式。這算是個(gè)小 hack 技巧,實(shí)際中應(yīng)該沒人這么干。
import React from 'react'
export default function TestIndeterminateComponent() {
return {
componentDidMount() {
console.log('invoker')
},
render() {
return <span>aaa</span>
},
}
}
6. HostRoot
該特殊類型對應(yīng)的是 FiberRoot 節(jié)點(diǎn)。
7. HostComponent & HostText
- HostComponent:原生 dom 節(jié)點(diǎn),也就是 jsx 中小寫的那種。
- HostText:文本節(jié)點(diǎn)。
8. PortalComponent
獨(dú)特地方在于其需要有單獨(dú)的掛載點(diǎn)。
9. ForwardRef
- 下次更新傳入的 ref 如果沒變化,會跳過當(dāng)前節(jié)點(diǎn)的更新(
bailoutOnAlreadyFinishedWork)。 - 要注意被 ForwardRef 包裹后的組件內(nèi)部獲取不到外部提供的 context。
- 然后同樣是調(diào)和子節(jié)點(diǎn),根據(jù)調(diào)用 render 得到新的 react element,調(diào)和為相應(yīng)的 fiber 節(jié)點(diǎn)。
10. Mode
- ConCurrentMode
- StrictMode
這樣的組件類型其實(shí)只是一種標(biāo)記,在 Fiber 的 mode 屬性(通過位運(yùn)算)上進(jìn)行記錄,在后面的創(chuàng)建更新時(shí),mode 作為計(jì)算不同的 expirationTime 的依據(jù)。
11. MemoComponent
本質(zhì)的更新邏輯和 FunctionalComponent 一樣,只是多了一步對新老 props 的 shallowEqual 淺比較,從而有機(jī)會跳過本次更新。
LazyComponent 和 SuspenseComponent 后面單獨(dú)研究。