Render階段 - 協調器 - Reconciler工作
render階段
開始於performSyncWorkOnRoot
或者performConcurrentWorkOnRoot
.- 取決於同步還是異步更新
// performSyncWorkOnRoot會調用該方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot會調用該方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
- shouldYield: 判斷瀏覽器當前是否存在剩餘時間.
- workInProcess: 代表當前已經創建的
workInProcess fiber
- performUnitOfWork:
- 創建下一個
Fiber節點
, 並賦值給workInProcess
- 並將
workInProcess
與已經創建的Fiber節點
鏈接起來, 構成一個fiber樹
- 創建下一個
Fiber Reconciler
重構了Stack Reconciler
, 通過遍歷實現可中斷的遞歸- performUnitOfWork. 工作分爲"遞"和"歸"
- "遞"階段
- 從
rootFiber
開始向下深度優先遍歷. 爲每個fiber節點
調用beginWork
方法 - 該方法會根據傳入的
Fiber節點
創建子Fiber節點
, 並將兩個Fiber
節點鏈接起來. - 如果有兄弟節點, 並根據節點, 依次創建兄弟節點
- 遍歷到葉子節點時, 會進入歸階段.
- 從
- "歸"階段
- 歸節點調用
completeWork
階段, 處理fiber節點
- 某個fiber節點完成執行完
completeWork
- 如果其存在
兄弟Fiber節點
, 會進入兄弟節點的遞階段 - 如果不存在
兄弟fiber節點
, 會進入父級Fiber
的歸階段
- 如果其存在
- 歸節點調用
- "遞"和"歸"會交替執行, 直到"歸"到
rootFiber
.render階段
的工作完成
單一文本的葉子節點不會執行
beginWork/completeWork
beginWork
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...省略函數體
}
-
current: 當前組件對應的
Fiber節點
在上一次更新時的Fiber節點
.workInProcess.alternate
-
workInProcess: 當前組件對應的
fiber節點
-
renderLanes: 優先級相關
-
區分mount還是update
- mount時:
current === null
- update時:
current !== null
- mount時:
-
begin工作分爲兩部分:
- update時:
current節點
存在, 滿足一定條件, 進行復用. 克隆current.child
到workInProcess.child
- mount時: 除了
fiberRootNode
以外,current === null
. 根據fiber.tag
不同, 創建不同類型的子fiber節點
- update時:
-
Update: 滿足如下情況. ->
didReceiveUpdate = false;
直接複用上一次的fiber節點
oldProps === newProps && workInProcess.type === current.type
不變includesSomeLane(renderLanes, updateLanes)
, 當前節點的優先級不夠
-
Mount:
- 根據不同類型的tag, 進入不同類型的
Fiber
的創建邏輯 - 最終都會執行
reconcileChildren
: 根據fiber節點
創建子fiber節點
- 根據不同類型的tag, 進入不同類型的
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// update時:如果current存在可能存在優化路徑,可以複用current(即上一次更新的Fiber節點)
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
switch (workInProgress.tag) {
// 省略處理
}
// 複用current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = false;
}
// mount時:根據tag不同,創建不同的子Fiber節點
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他類型
}
}
- reconcileChildren工作:
mount
: 創建新的子fiber節點
update
: 會將當前fiber節點
和上次更新的fiber節點
對比(diff), 產生結果比較新的fiber節點
- 最終都會生成新的
子fiber節點
, 賦值給workInProcess.child
, 作爲本次beginWork
的返回值 - 並作爲下一次:
performUnitOfWork
執行時workInProcess
的傳參 reconcileChildFibers
會爲生成的Fiber節點
帶上effectTag
屬性
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 對於mount的組件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 對於update的組件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
- effectTag
- render(Reconciler)階段時在內存中工作的. 工作接受後, 通知
Renderer
進行dom操作 - dom的操作保存在
effectTag
中
- render(Reconciler)階段時在內存中工作的. 工作接受後, 通知
- 通知
Renderer
將Fiber節點
對應的dom插入到頁面中需要滿足兩個條件fiber.stateNode
存在, 即fiber節點
保存了對應的dom節點
Fiber節點
上存在Placement effectTag
fiber.state
Node會在completeWork
中完成- mount時只用
rootFiber
會被賦值Placement effectTag
.commit
階段一次性插入完成
commitWork
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
// ...省略
return null;
}
case HostRoot: {
// ...省略
updateHostContainer(workInProgress);
return null;
}
case HostComponent: {
// ...省略
return null;
}
// ...省略
- HostComponent: 原生組件對應的
fiber節點
- Update時:
Fiber節點
對應的dom節點已經存在了, 不需要生成dom節點, 主要處理propsonClick, onChange
等回調函數的註冊- 處理
style prop
- 處理
DANGEROUSLY_SET_INNER_HTML prop
- 處理
children prop
- 主要調用
updateHostComponent
- 處理完的
props
賦值給workInProcess.updateQueue
- 並最終會在
commit階段(Renderer)
渲染在頁面上 workInProgress.updateQueue = (updatePayload: any);
updatePayload
爲數組, 偶數爲變化的prop key
, 奇數爲prop value
- 處理完的
- Mount時
- 主要邏輯:
- 爲
fiber節點
增加對應的dom節點 - 將子孫dom節點插入到剛生成的dom節點上
- 與update時中的
updateHostComponent
類似的props
處理過程
- 爲
- 主要邏輯:
- completeWork階段屬於歸階段函數. 每次調用
appendAllChildren
時, 都會將已生成的子孫dom節點插入到當前生成的dom節點下. - "歸"到
rootFiber
時, 已經有一顆已經構建好的內存中的dom樹
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// update的情況
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
} else {
// mount的情況
// ...省略服務端渲染相關邏輯
const currentHostContext = getHostContext();
// 爲fiber創建對應DOM節點
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 將子孫DOM節點插入剛生成的DOM節點中
appendAllChildren(instance, workInProgress, false, false);
// DOM節點賦值給fiber.stateNode
workInProgress.stateNode = instance;
// 與update邏輯中的updateHostComponent類似的處理props的過程
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
return null;
}
- effectList
- 解決需要所有
fiber節點
, 找到標記effectTag
的fiber
completeWork
的上層函數completeUnitOfWork
中:- 每次執行完
completeWork
, 且存在effectTag
的fiber節點
, 會保存在effectList
單向鏈表中 - 第一個節點保存在
fiber.firstEffect
- 最後一個節點保存在
fiber.lastEffect
- 在"歸"階段, 所有
effectTag的Fiber節點
, 都會追加到這條鏈表中 - 最終形成一條以
rootFiber.firstEffect
爲起點的effectList
commit階段
只需要遍歷effectList
即可
nextEffect nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber
-
render階段
全部完成工作後.performSyncWorkRoot
函數中,fiberNode
傳遞給commitRoot
.- 開啓
commit階段
的工作
Commit階段 - 渲染器 - Renderer器工作
fiberRootNode
作爲傳參fiberRootNode.firstEffect
上保存了一條需要執行副作用
的Fiber節點單向鏈表
- 這些fiber節點的
updateQueue
上保存了變化的props - 這些副作用對應的dom操作, 在
commit階段
執行 - 一些聲明週期鉤子函數(componentDidXXX), hook(useEffect)在
commit階段執行
. commit階段
的主要工作before mutation階段
: 執行dom操作前mutation階段
: 執行dom操作layout階段
: 執行dom操作後
before mutation之前
和layout之後
還有一些工作useEffect
的觸發優先級相關
重置ref
的綁定的解綁
before mutation
之前- 變量賦值, 狀態重置
- 最後會賦值一個
firstEffect
,commit
三個子階段都會用到
layout
之後useEffect
相關處理- 性能追蹤相關
- 在
commit
出發一些生命週期函數, hook
before mutation
- 遍歷
effectList
並調用commitBeforeMutationEffects
函數處理
// 保存之前的優先級,以同步優先級執行,執行完畢後恢復之前優先級
const previousLanePriority = getCurrentUpdateLanePriority();
setCurrentUpdateLanePriority(SyncLanePriority);
// 將當前上下文標記爲CommitContext,作爲commit階段的標誌
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// 處理focus狀態
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;
// beforeMutation階段的主函數
commitBeforeMutationEffects(finishedWork);
focusedInstanceHandle = null;
commitBeforeMutationEffects
- 處理
DOM節點
渲染/刪除後的autoFocus
,blur
的邏輯 - 調用
getSnapshotBeforeUpdate
生命週期鉤子 - 調度
useEffect
- 處理
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
// ...focus blur相關
}
const effectTag = nextEffect.effectTag;
// 調用getSnapshotBeforeUpdate
if ((effectTag & Snapshot) !== NoEffect) {
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
// 調度useEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
// 以某個優先級調用一個回調函數
scheduleCallback(NormalSchedulerPriority, () => {
// 調用useEffect
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
- 調用
getSnapshotBeforeUpdate
render階段
的任務可能被中斷重新開始,componentWillXXX
鉤子可能被多次觸發. 變爲不安全的- 使用新的
getSnapshotBeforeUpdate
, 在commit階段
內同步執行, 所以只會觸發一次
- 異步調度
flushPassiveEffects
方法從全局變量rootWithPendingPassiveEffects
獲取effectList
effectList
包含了需要執行副作用的Fiber節點
- 插入dom節點(PLACEMENT)
- 更新dom節點(UPDATE)
- 刪除dom節點(DELETION)
- 當一個
FunctionComponent
含有useEffect
或者useLayoutEffect
時, 它的fiber節點也會含有Passive effectTag
flushPassiveEffect
內部遍歷effectList
, 執行effect回調函數
useEffect
分成三步:before mutation階段
在scheduleCallback
中調度flushPassiveEffects
layout階段
之後, 將effectList
賦值給rootWidthPendingPassiveEffects
scheduleCallBack
觸發flushPassiveEffects
,flushPassiveEffects
內部遍歷rootWidthPendingPassiveEffects
- 原因:
- 在瀏覽器完成渲染和繪製以後, 傳給
useEffect
的函數會延遲調用. - 使得它試用於許多瀏覽器常見的副作用場景, 比如設置訂閱和事件處理等情況
- 因此不應該在函數中執行阻塞瀏覽器更新屏幕的操作
- 防止同步執行時阻塞瀏覽器渲染
- 在瀏覽器完成渲染和繪製以後, 傳給
mutation階段
- 同樣遍歷
effectList
, 執行函數,commitMutationEffects
nextEffect = firstEffect;
do {
try {
commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
commitMutationEffects
- 根據
ContentRest effectTag
重置文本節點 - 更新
ref
- 根據
effectTag
分別處理, 其中effectTag
分別包括(Placement|Update|Deletion|Hydrating
)
- 根據
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
// 遍歷effectList
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 根據 ContentReset effectTag重置文字節點
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
// 更新ref
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// 根據 effectTag 分別處理
const primaryEffectTag = effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
// 插入DOM
case Placement: {
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
break;
}
// 插入DOM 並 更新DOM
case PlacementAndUpdate: {
// 插入
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
// 更新
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// SSR
case Hydrating: {
nextEffect.effectTag &= ~Hydrating;
break;
}
// SSR
case HydratingAndUpdate: {
nextEffect.effectTag &= ~Hydrating;
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 更新DOM
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 刪除DOM
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
Placement effect
:fiber節點
對應的dom節點
需要插入到頁面中- 獲取父級dom節點
- 獲取fiber的兄弟節點,
getHostSibling
非常耗時, 因爲同一個fiber節點
不止包含HostComponent
* - 根據dom節點是否存在, 判斷執行
parentNode.insertBefore
或者parentNode.appendChild
Update effect
:fiber節點
需要更新, 調用commitWork
FunctionComponent mutation
- 當
fiber.tag
爲FunctionComponent
, 會調用commitHookEffectListUnmount
. - 遍歷
effectList
, 執行所有useLayoutEffect hook
的銷燬函數
- 當
HostComponent mutation
- 調用
commitUpdate
- 最終在
updateDOMProperties
中將render階段 completeWork
中爲fiber節點
賦值的updateQueue
對應的內容, 渲染在頁面上
- 調用
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
// 處理 style
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
// 處理 DANGEROUSLY_SET_INNER_HTML
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
// 處理 children
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {
// 處理剩餘 props
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
- Deletion Effect: 當
fiber節點
含有effectTag
時, 調用commitDeletion
- 遞歸調用
fiber節點
以及子孫fiber節點
. 調用fiber.tag
爲ClassComponent
的componentWillUnComponent
. - 從頁面中移除
fiber節點
對應的dom節點
- 解綁
ref
- 調度
useEffect
的銷燬函數
- 遞歸調用
layout
- 代碼是在dom渲染完成後執行的
- 該階段觸發的生命週期鉤子和
hook
可以直接訪問到已經改變後的dom
- 遍歷
effectList
, 執行函數
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 調用生命週期鉤子和hook
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
// 賦值ref
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
-
commitLayoutEffects
commitLayoutEffectOnFiber
調用生命週期鉤子和hook相關操作ClassComponent
調用ComponentDidMount
/ComponentDidUpdate
- 調用
this.state()
第二個參數函數 FunctionComponent
調用useLayoutEffect
回調函數, 調度useEffect
的銷燬和回調函數useLayoutEffect()
: 從上一次更新的銷燬到這次更新執行, 是同步執行的useEffect()
: 需要先調度, 在Layout階段
執行完成後, 再異步執行
commitAttachRef
: 賦值ref: 獲取dom實例, 更新dom
-
mutation階段
執行後,layout
開始前, 切換root.current = finishWork
-
因爲layout執行的生命週期函數和hook需要獲取新的dom信息