多種不同類型的組件的更新過程嗦枢,以及如何遍歷節(jié)點(diǎn)形成新的 Fiber 樹谚攒,即 reconcilerChildren 調(diào)和子節(jié)點(diǎn)的過程止毕。
-1. 入口和優(yōu)化
- 判斷組件更新是否可以優(yōu)化
- 根據(jù)節(jié)點(diǎn)類型分發(fā)處理
- 根據(jù) expirationTime 等信息判斷是否可以跳過
幫助優(yōu)化整個樹的更新過程的方法牌芋。
只有 ReactDOM.render() 的時才會更新 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)车猬。這就是個不斷向下遍歷節(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 遍歷的時候再真正處理伐厌。
接下來是各種組件類型的更新承绸,也就是調(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:
之前說過每個 Fiber 節(jié)點(diǎn)上的 type 就是指 createReactElement 時傳入第一個參數(shù),即 函數(shù)/class/原生dom標(biāo)簽字符串/內(nèi)置的某些類型(如React.Fragment 什么的刃唐,大多數(shù)時候會是個 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 時會把處理好的 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ù)雜的在這個方法中
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}
2. reconcileChildren
- 根據(jù) reactElement 上的 props.children 生成 fiber 子樹。
- 判斷 Fiber 對象是否可以復(fù)用回梧。因?yàn)橹挥械谝淮问钦w全部渲染废岂,而后續(xù)更新時自然要考慮復(fù)用。
- 列表根據(jù) key 優(yōu)化狱意。
- 最終迭代處理完整個 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 的是臨時的節(jié)點(diǎn)包各,渲染更新時要被跳過摘仅,newChild = newChild.props.children靶庙,也就是把下一層的 children 賦值為當(dāng)前某個待更新的組件的 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)時要看子節(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):
只看第一個節(jié)點(diǎn)是不是文本節(jié)點(diǎn):
- 如果此前老的第一個子節(jié)點(diǎn)也是文本節(jié)點(diǎn)捅膘,那么就復(fù)用留著添祸,而刪除相鄰節(jié)點(diǎn),因?yàn)楝F(xiàn)在要更新為文本節(jié)點(diǎn)了寻仗,所以留一個節(jié)點(diǎn)就夠用了刃泌。
- 如果不是,那么就整個刪除老的子節(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镶殷。
之前說過更新分兩個階段禾酱,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ù)組(如使用 Array.prototype.map),需要為每個子項(xiàng)指定 key 值吧慢。
**
以相同順序分別遍歷新老 children涛漂,對比 key 是否相同 來決定是否復(fù)用老的 fiber 節(jié)點(diǎn):
直到遇到 key 開始不相同了,就不再對標(biāo)著復(fù)用检诗,而此時 props.children 也就是 react element 的數(shù)組還有剩余匈仗,也就是還沒全部轉(zhuǎn)化為 fiber。那么有兩種情況:
- 對位的老的子節(jié)點(diǎn) oldFiber 已經(jīng)用完了逢慌,那么就為剩余未轉(zhuǎn)換的 react element 每個都單獨(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ù)用音比。這說明本次更新中俭尖,某個節(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
在最初第一次渲染時斗搞,對于所有的 functionalComponent 都初始標(biāo)記為 IndeterminateComponent 類型愉镰,
然后主要根據(jù)其返回的 value 中是否有 render 方法,從而才將 workInProgress.tag 其進(jìn)一步明確為 ClassComponent 還是 FunctionComponent伴栓。
基于內(nèi)部這種判斷邏輯贸宏,我們竟然可以通過在函數(shù)式組件中返回的對象上提供 render 函數(shù)颓哮,以此將函數(shù)式組件“模擬”出了 class 組件的形式磕仅。這算是個小 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)建更新時,mode 作為計(jì)算不同的 expirationTime 的依據(jù)。
11. MemoComponent
本質(zhì)的更新邏輯和 FunctionalComponent 一樣枉圃,只是多了一步對新老 props 的 shallowEqual 淺比較功茴,從而有機(jī)會跳過本次更新。
LazyComponent 和 SuspenseComponent 后面單獨(dú)研究孽亲。