⚠️1.1萬長文⚠️ React源碼並非洪水猛獸,知道方法,就可以很輕易地馴服它(=^▽^=)。文章基於最新的React源碼進行調試及閱讀,將以通俗地方式解讀React ...
1 前言
大家好,我是心鎖,一枚23屆準畢業生。
如果讀者閱讀過我其他幾篇React相關的文章,就知道這次我是來填坑的了
原因是,寫了兩篇解讀react-hook的文章後我發現——並不是每位同學都清楚React的架構,包括我在內也只是綜合不同技術文章與閱讀部分源碼有一個瞭解,但是調試時真正沉澱成文章的還沒有。
所以這篇文章來啦~文章基於2022年八九月的React源碼進行調試及閱讀,將以通俗的形式揭秘React
閱讀本文,成本與收益如下
閱讀耗時:26min+
全文字數:1w+
全文字元:5.5w+
預期收益:通明境 · React架構
本文適合有閱讀React源碼計劃的初學者或者正在閱讀React源碼的工程師,我們一起形成頭腦風暴。
2 認識Fiber節點
2.1 Fiber節點基礎部分
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
...
this.ref = null;
...
}
Fiber節點本身存儲了一些最基本的數據,其中包括如上六項構成Instance
,它們分別代表
-
tag:Fiber節點對應組件的類型,包括了Funtion、Class等
-
key:更新key會強制更新Fiber節點
-
type:保存組件本身。準確來說,對於函數組件保存函數本身,對於類組件保存類本身,對於HostComponent,也就是如原生<div></div>這類原生標簽會保存節點名稱
-
elementType:保存組件類型和type大部分情況是一樣的,但是也有不一樣的情況,比如
LazyComponent
-
stateNode:保存Fiber對應的真實DOM節點
-
ref: 和key一樣屬於base欄位
2.2 Fiber樹結構實現
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
...
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
...
}
我們看到Fiber節點這四個屬性,它們的含義分別是
- return:指向父節點Fiber
- child:指向子節點Fiber
- sibling:指向右邊的兄弟節點Fiber
這樣子一來,對於我們這裡的組件,就構成瞭如圖的Fiber樹
const CountButton = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(v => v + 1);
};
useEffect(() => {
console.log('Hello Mount Effect');
return () => {
console.log('Hello Unmount Effect');
};
}, []);
useEffect(() => {
console.log('Hello count Effect');
}, [count]);
return (
<>
<div>Render by state</div>
<div>{count}</div>
<button onClick={handleClick}>Add Count</button>
</>
);
};
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<CountButton/>
</header>
</div>
);
}
2.3 函數式組件&&Fiber
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
...
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
...
}
從源碼上看,React為hook足足騰出了五個屬性專門處理在函數式組件中使用hook的場景。
這些個玩意兒氣其實我們在前邊的hook章節也或多或少有瞭解過,這裡專門講述Fiber節點上存儲的這些結構的作用。
2.3.1 pendingProps
pendingProps,從FiberNode的構造函數看,是mixed(可傳入)進來的
也就是說,這部分props可以在Fiber間傳遞,主要用於更新/創造新Fiber節點時用來傳遞props
2.3.2 memoizedProps
memoizedProps
和pendingProps
的區別是什麼呢?
我們知道,props代表一個Function的參數,當props變化時Function也會再次執行。
一般來講,memoizedProps
會在整個渲染流程結尾部分被更新,存儲FiberNode的props。
而pendingProps
一般在渲染開始時,作為新的Props出現
舉個更便於理解的例子,在如圖的beginWork
階段,會對比新的props和舊的props來確定是否更新,此時比較的就是workInProgress.pendingProps
和current.memoizedProps
2.3.3 updateQueue
上一篇我們講useEffect
有講到,updateQueue
以如圖的形式存儲useEffect
運行時生成的各個effect
lastEffect以環形鏈的形式存儲了單個節點的所有effect。
(當然,這裡指的當然只是函數式組件)
2.3.4 memoizedState
在useState
章節,我們也有講過memoizedState
,memoizedState
存儲了我們調用hook時產生的hook
對象,目前已知除了useContext不會有hook對象產生並掛載,其他hook都會掛載到這裡。
hook之間以.next
相連形成單向鏈表。
而hook調用時產生的不管是effect(useEffect)還是state(useState),都是存儲在
hook.memoizedState
,體現在Fiber節點上,其實是存儲在hook.memoizedState.memoizedState
,註意不要混淆。
2.3.5 dependencies
以下是調試代碼
const BaseContext = createContext(1);
const BaseContextDemo = () => {
const {base} = useContext(BaseContext);
return <div>{base}</div>;
};
const CountButton = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(v => v + 1);
};
useEffect(() => {
console.log('Hello Mount Effect');
return () => {
console.log('Hello Unmount Effect');
};
}, []);
useEffect(() => {
console.log('Hello count Effect');
}, [count]);
const ref = useRef();
const [base, setBase] = useState(null);
const initValue = {
base,
setBase,
};
return (
<BaseContext.Provider value={initValue}>
<div ref={ref}>
<div>Render by state</div>
<div>{count}</div>
<button onClick={handleClick}>Add Count</button>
<button onClick={() => setBase(i => ++i)}>Add Base</button>
<BaseContextDemo />
</div>
</BaseContext.Provider>
);
};
在還沒有發出的useContext
原理中,會記載useContext的實現原理,劇透就是FiberNode.dependencies
這個屬性記載了組件中通過useContext
獲取到的上下文
從調試結果看,多個context也將通過.next
相連,同時顯然,這是一條單向鏈表
2.4 操作依據
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
...
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
...
}
我們看到這三個屬性
-
deletions:待刪除的子節點,render階段diff演算法如果檢測到Fiber的子節點應該被刪除就會保存到這裡。
-
flags/subtreeFlags:都是二進位形式,分別代表
Fiber
節點本身的保存的操作依據與Fiber
節點的子樹的操作依據。
flags是React中很重要的一環,具體作用是通過二進位在每個Fiber節點保存其本身與子節點的flags。
至於具體如何保存,實際上是使用了二進位的特性,舉幾個例子
2.4.1 &運算
溫習一下
&運算符
的規則:只有1&1=1,其他情況為0
const NoFlags = /* */ 0b000000000000000000000000;
const PerformedWork = /* */ 0b000000000000000000000001;
const Placement = /* */ 0b000000000000000000000010;
const Update = /* */ 0b000000000000000000000100;
const unknownFlags=Placement;
Boolean(unknownFlags & Placement) // true
Boolean(unknownFlags & Update) //false
React中會用一個未知的flags & 一個flag,此時是在判斷未知的flags中是否包含flag。
之所以說是是否包含,我們可以看看下邊的代碼。
const NoFlags = /* */ 0b000000000000000000000000;
const PerformedWork = /* */ 0b000000000000000000000001;
const Placement = /* */ 0b000000000000000000000010;
const Update = /* */ 0b000000000000000000000100;
const unknownFlags = Placement|Update; //此時=0b000000000000000000000110
Boolean(unknownFlags & Placement) // true
Boolean(unknownFlags & Update) //true
2.4.2 |運算
溫習一下
|運算符
的規則:只有0&0=0,其他情況為1
上邊unknownFlags的例子我們不難發現,react利用了|運算符
的特性來存儲flag
const unknownFlags = Placement|Update; //此時=0b000000000000000000000110
這樣的好處是快,判斷是否包含的時候,直接使用& 運算符
,在有限的操作依據面前,使用二進位完全可以兜住所有情況。
2.4.3 ~運算
~運算符會把每一位取反,即1->0,0->1
在React中,~運算符同樣是常用操作
那麼作用是什麼呢?其實也很容易從函數上下文分析出來,對於圖中這個例子,react通過~運算符
與&運算符
的結合,從flags中刪除了Placement
這個flag。
2.4.4 小總結:React中常見的操作
-
通過
unknownFlags & Placement
判斷unknownFlags
是否包含Placement
-
通過
unknownFlags |= Placement
將Placement
合併進unknownFlags
中 -
通過
unknownFlags &= ~Placement
將Placement
從unknownFlags
中刪去
關於有哪些flags,我們可以翻閱到
ReactFiberFlags.js
,這裡會有詳細flags的記載
2.5 雙緩存樹的體現
我們曾說過,React的最基本工作原理雙緩存樹,這引申出了我們需要知道這種機制在React中的實際體現。
這需要我們找到ReactFiber.old.js
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
...
this.alternate = null;
...
}
由此我們知道,FIberNode上會有一個屬性alternate
,而這個屬性正是我們期望的雙緩存樹中,里樹與外樹的雙向指針。
正如圖所見,在初次渲染中,current===null
,所以目前仍是白屏,而workInProgress
已經在構建
(圖誤,在renderWithHooks才對)
而當我們再次渲染,在renderWithHooks
斷點,就可以觀察到workInProgress.alternate==current
2.6* 優先順序相關
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
...
this.lanes = NoLanes;
this.childLanes = NoLanes;
...
}
和lane有關的變數統一和調度優先順序有關,暫時不涉及(因為還沒看)
2.7* React devtools Profiler
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
...
if (enableProfilerTimer) {
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN;
this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}
...
}
React並不只是react
,react倉庫里包含了其他工程,其中就包含了我們的react profiler工具,在使用了profiler工具的情況下,react fiber會記錄一些運行時間,其實很多帶有Profiler
的判斷語句都是和Profiler在配合。
3 好好認識hook結構
我們上邊有講到FiberNode.memoizedState
,我們知道這裡保存的是mountWorkInProgressHook
時產生的hook對象
{
memoizedState: 0,
baseState: 0,
baseQueue: null,
queue: ???,
next:null
}
那麼hook的各個項指什麼?
3.1 baseState和memoizedState
其實很好理解,baseState對應上一次的state(effect),memoizedState為最新的state(effect),總之就是hook保存基本數據的地方。
3.2 queue
而hook.queue則是useState、useReducer
的dispatcher存儲的地方。
var queue:UpdateQueue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: initialState
};
hook.queue = queue;
var dispatch = queue.dispatch = dispatchReducerAction.bind(null, currentlyRenderingFiber$1, queue);
對於queue的結構,我們逐一講解
3.2.1 lastRenderedState & lastRenderedReducer
- queue.lastRenderedState屬性存儲上一個 state
- queue.lastRenderedReducer 屬性存儲 reducer 內部狀態變更邏輯
其中queue.lastRenderedReduce
可能不好理解,我們可以從代碼中理解,且看這裡
function basicStateReducer(state, action) {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
function mountState(initialState) {
...
hook.memoizedState = hook.baseState = initialState;
var queue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
...
}
這是dispatchSetState
中的一段邏輯,處理的正是我們下邊將講述的,「不在渲染中」的處理階段(onClick觸發===非同步觸發)。
那這裡可以看到,我們可以從lastRenderedReducer
得到eagerState
var currentState = queue.lastRenderedState;
var eagerState = lastRenderedReducer(currentState, action); // Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
eagerState是什麼? 實際上這裡是通過lastRenderedReducer快速獲得了最近一次的state。
react會通過objectIs(eagerState,currentState)
來確定是否不進行更新,這也是為什麼我們更新state的時候要註意state為不可變數據,每次更新都需要更新一個新值才有效
if (objectIs(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
3.2.2 dispatch
dispatch 屬性存儲狀態變更函數,對應useState、useReducer 返回值中的第二項
function mountState(initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
hook.queue = queue;
var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
值得註意的就是dispatch會通過.bind事先註入currentlyRenderingFiber$1, queue
兩個參數,此間通過bind綁定的currentlyRenderingFiber$1
,作用是判斷這個更新是在fiber的render階段還是非同步觸發。
這也給了我們一個判斷fiber在render階段的條件
function isRenderPhaseUpdate(fiber: Fiber) { const alternate = fiber.alternate; return ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber) ); }
3.2.3 pending
pending 屬性存儲排隊中的狀態變更規則,單向環形鏈表結構。
在源碼中,每一個規則以Update
的結構連接
export type Update<S, A> = {|
lane: Lane,
action: A,
hasEagerState: boolean,
eagerState: S | null,
next: Update<S, A>,
|};
那麼我們知道了
- eagerState 緩存上一個狀態(React稱之為急迫的狀態)
- action 代表狀態變更的規則,可以是本次要被修改的值,也可以是函數
- hasEagerState 則是記錄是否執行過優化邏輯
eagerState在所有源碼中只在這裡使用,根據React源碼,這裡的優化指的是React會在eagerState===currentState的情況下,不做重渲染。如果狀態更新前後沒有變化,則可以略過剩下的步驟。
try {
var currentState = queue.lastRenderedState;
var eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (objectIs(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {
} finally {
{
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
3.3 baseQueue
值得註意的是,baseQueue的結構來自queue.pending而不是queue
(baseQueue被賦值queue.pending)
其餘的大抵是沒啥好說的,baseQueue在調試中的體現我暫時並沒有遇到,推測需要有比較大量的更新。
4 React架構
本章我們講述React的渲染流程,將覆蓋React的render
階段與commit
階段的概念與流程概覽,不會非常深入,爭取留存印象。
4.1 React渲染關鍵節點
我們已經預先知道可以將React的渲染分成render
階段和commit
階段,也知道render
階段的關鍵函數是beginWork
和completeWork
,commit
階段的關鍵函數則是commitRoot
。
在這個基礎上,我們從調用堆棧中可以找到這兩個階段的起始節點。
- render階段
我們在beginWork中打上斷點,然後可以回溯調用堆棧找到出發點。
從圖中,我們可以知道renderRoot觸發於performConcurrentWorkOnRoot
除此之外,在performSyncWorkOnRoot
中也可以走入renderRoot
它們會根據情況走到renderRootConcurrent
或者renderRootSync
,這裡即是render階段的開始點
那麼我們得到第一個關鍵節點:
- render階段開始於
renderRootConcurrent
或renderRootSync
- commit階段
我們知道,render階段的尾巴是completeWork
,commit階段的起步是commitRoot
,我們嘗試在這completeWork
方法中斷點,然後單步調試到commitRoot
。
上圖是我debug出來的結果,completeWork
與commitRoot
之間的最近公共函數節點是performSyncWorkOnRoot/performConcurrentWorkOnRoot
。
那麼我們知道,commitRoot
即是commit階段的起點。
那麼我們得到兩個關鍵信息:
- commit階段開始於
commitRoot
- render階段和commit階段通過
performSyncWorkOnRoot/performConcurrentWorkOnRoot
聯動
4.1.1 小總結
- render階段開始於
renderRootConcurrent
或renderRootSync
- commit階段開始於
commitRoot
- render階段和commit階段通過
performSyncWorkOnRoot/performConcurrentWorkOnRoot
聯動
4.2 狀態更新流程
4.2.1 找到root節點
正常render的第一步,是找到當前Fiber的root節點。
以useState造成的渲染舉例,React會通過enqueueConcurrentHookUpdate->getRootForUpdatedFiber
找到當前節點的root節點。
function dispatchSetState(fiber, queue, action) {
...
var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
var eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
...
}
function getRootForUpdatedFiber(sourceFiber) {
...
detectUpdateOnUnmountedFiber(sourceFiber, sourceFiber);
var node = sourceFiber;
var parent = node.return;
while (parent !== null) {
detectUpdateOnUnmountedFiber(sourceFiber, node);
node = parent;
parent = node.return;
}
return node.tag === HostRoot ? node.stateNode : null;
}
尋找root節點是一個向上不斷尋找root節點的過程,在這個過程中react還會持續調用detectUpdateOnUnmountedFiber
檢查是否調用了過期的更新函數。
什麼是過期的更新函數?舉個例子,通過useRef保存了setState方法,但是隨著組件更新ref中的setState方法並沒有更新,此時由於setState方法本質上是通過.bind的形式報存了函數及參數fiber節點,此時就會存在調用了一個已卸載組件的過期的setState方法。
4.2.2 調度同步/非同步更新
找到root節點之後,那麼就要進入render流程
,這就存在一個問題。
我們上邊說了,render
階段的觸發函數是performSyncWorkOnRoot
或performConcurrentWorkOnRoot
,那麼如何判斷應該進入同步更新還是非同步更新呢?
這就要走到ensureRootIsScheduled
,ensureRootIsScheduled
會通過判斷newCallbackPriority === SyncLane
來確定走同步render還是非同步render,這裡涉及調度器,暫時不講(還沒看還不會)
function ensureRootIsScheduled(root, currentTime) {
...
var newCallbackNode;
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
if (root.tag === LegacyRoot) {
...
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
...
newCallbackNode = null;
} else {
var schedulerPriorityLevel;
...
newCallbackNode = scheduleCallback$2(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
那麼可以看到,這裡會有一個scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root))
或者scheduleCallback$2(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root))
的過程。
值得註意的是,同步調度這裡還更複雜,react一方面需要考慮是否是嚴格模式做不同的callback
(ensureRootIsScheduled是一個很重要的函數,會Scheduled一起講會比較好)
另一方面還調度了flushSynCallbacks
,這個函數做的事情很簡單,就是把syncQueue中的待執行任務全部執行
4.2.3 render階段
render階段分成了兩個階段,我們在狀態更新流程中不講細節,只講明基本作用,細節請看後邊的單章
經歷了調度更新,會來到render階段,render階段做了兩件事。
beginWork
階段。在這個階段react做的事情是從root遞歸到子葉,每次beginWork
會對Fiber
節點進行新建/復用邏輯,然後通過reconcileChildren
將child Fiber
掛載到workInProgress.child
併在child Fiber
上記錄flags,最終遍歷整個Fiber樹completeWork
階段。在這個階段,是從子葉不斷向上遍歷到父親Fiber節點的過程,這個過程中,completeWork
會把workInProgress Tree
上的真實DOM掛載/更新上去。
那麼總結來說,beginWork
負責虛擬DOM節點Fiber Node
的維護與flag記錄,completeWork負責真實DOM節點在Fiber Node
的映射工作。
當然,這些操作只涉及節點維護,真正渲染到頁面上就是commit階段要負責的了
4.2.4 commit階段
commit階段,除了會處理一下和hook
相關的事情之外,最主要做了就是負責把beginWork階段記錄的flags在真實DOM樹上進行操作。
總結來說:
- 處理和
useEffect\useInsertionEffect\useLayoutEffect
相關的hook,處理class組件相關的生命周期鉤子 - 基於flags做真實DOM樹操作,包括增刪改,以及輸入框類型節點的focus、blur等問題
- 清理一些全局變數,並確保進入下一次調度
4.3 render階段
這裡是延續狀態更新流程的render階段。
我們在狀態更新第一步就拿到了root節點,經過調度更新後會進入render階段。
此時我們有兩種走法,一種是通過renderRootSync
來到workLoopSync
,另一種則是通過renderRootConcurrent
走到workLoopConcurrent
,這兩者的區別是workLoopConcurrent
會檢查瀏覽器是否有剩餘時間片。
function workLoopConcurrent() {
// 執行工作,直到調度程式要求我們讓步
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
function workLoopSync() {
// 已經超時了,因此無需檢查我們是否需要讓步就可以執行工作
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
workLoop做了什麼呢?這就要從performUnitOfWork(workInProgress)
說起,下邊的代碼是精簡邏輯 (只剩下beginWork這部分邏輯) 過後的performUnitOfWork
函數,可以看到performUnitOfWork
通過beginWork
創建了一個新的節點賦給workInProgress
。
function performUnitOfWork(unitOfWork) {
var current = unitOfWork.alternate; // currentFiber
setCurrentFiber(unitOfWork); // 會將全局current變數設定為workInProgressFiber
var next = beginWork$1(current, unitOfWork, renderLanes$1); // currentFiber
resetCurrentFiber(); // 重置current變數為null
unitOfWork.memoizedProps = unitOfWork.pendingProps;
workInProgress = next;
...
}
4.3.1 beginWork
那麼此處引出了render階段中最重要的兩個函數之一beginWork
,beginWork正如上邊所說,這個函數的職責是返回一個Fiber節點,這個節點可以復用currentFiber
也可以創建一個新的。
我們其實在【useState原理】章節中有見過beginWork,當時我們強調了雙緩存機制,這次我們可以更細地瞭解一下beginWork。
我們提煉一下beginWork的核心邏輯,會發現beginWork
通過current!==null
來判斷是否是第一次執行,這裡的邏輯是如果是第一次執行,那麼Fiber沒有mount,自然為null。
function beginWork(current, workInProgress, renderLanes) {
...
if (current !== null) {
var oldProps = current.memoizedProps;
var newProps = workInProgress.pendingProps;
if (oldProps !== newProps || hasContextChanged() || (
workInProgress.type !== current.type )) {
didReceiveUpdate = true;
} else {
var hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
if (!hasScheduledUpdateOrContext &&
(workInProgress.flags & DidCapture) === NoFlags) {
// 沒有待更新的updates或者上下文信息,復用上次的Fiber節點
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
}
...
}
} else {
didReceiveUpdate = false;
...
}
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
...
case FunctionComponent:
...
case HostComponent:
...
}
}
#1 update復用邏輯
看到這裡,react在update的邏輯中,根據三個條件來判斷是否復用上一次的FIber
-
oldProps !== newProps,代表
props
是否變化 -
hasContextChanged(),
var didPerformWorkStackCursor = createCursor(false); // Keep track of the previous context object that was on the stack. // We use this to get access to the parent context after we have already // pushed the next context provider, and now need to merge their contexts.
-
workInProgress.type !== current.type,
fiber.type
是否變化
function beginWork(current, workInProgress, renderLanes) {
...
if (current !== null) {
var oldProps = current.memoizedProps;
var newProps = workInProgress.pendingProps;
if (oldProps !== newProps || hasContextChanged() || (
workInProgress.type !== current.type )) {
didReceiveUpdate = true;
} else {
//此處是復用的邏輯
...
}
} else {
didReceiveUpdate = false;
...
}
...
}
#2 mount/update新建邏輯
不滿足更新條件的話,會根據workInProgress.tag
新建不同類型的Fiber節點。對於不進行Fiber復用到更新也會進入這個邏輯
switch (workInProgress.tag) {
case IndeterminateComponent:
{
return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
}
case LazyComponent:
{
var elementType = workInProgress.elementType;
return mountLazyComponent(current, workInProgress, elementType, renderLanes);
}
case FunctionComponent:
{
var Component = workInProgress.type;
var unresolvedProps = workInProgress.pendingProps;
var resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderLanes);
}
case ClassComponent:
{
var _Component = workInProgress.type;
var _unresolvedProps = workInProgress.pendingProps;
var _resolvedProps = workInProgress.elementType === _Component ? _unresolvedProps : resolveDefaultProps(_Component, _unresolvedProps);
return updateClassComponent(current, workInProgress, _Component, _resolvedProps, renderLanes);
}
...
}
根據我們在【useState】章節的收穫,不管是update還是mount都要走到reconcileChildren
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
// mount時
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
// update時
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
}
}
這裡做的事情描述起來是比較好辦的,不過詳細起來就涉及diff演算法需要開單章
- mount時,創建新的Child Fiber節點
- update時,將當前組件與該組件在上次更新時對應的
Fiber
節點進行diff比較,將比較的結果生成新Fiber
節點
當然,不管走到哪裡,workInProgress都會得到一個child FIber
不管是reconcileChildFibers
還是mountChildFibers
,都是通過調用ChildReconciler這個函數來運行的。
而在整個ChildReconciler中,我們會經常性看到如圖一樣的操作。
這便引出了操作依據一說,react用Fiber.flags
並以二進位的形式存貯了對於每個Fiber的操作依據,這種方式比數組更高效,可以方便地使用位運算發為Fiber.flags
增刪不同的操作依據。
點擊這裡可以查看所有的操作類型
#3 diff演算法*
標記這個知識點,下次再說
4.3.2 completeWork
我們持續執行workLoop,會發現workInProgress
從rootFiber
持續深入到了我的調試代碼中的最底層(一個div),此時就到了render階段的第二個階段completeWork
。
function performUnitOfWork(unitOfWork) {
...
if (next === null) {
// 進入completeWork
completeUnitOfWork(unitOfWork);
} else {
...
}
...
}
那麼此時進入completeUnitOfWork
,這裡的核心邏輯是completeWork從子節點不斷訪問workInProgress.return
向上迴圈執行beginWork
,如果遇到兄弟子節點,則會將workInProgress指向兄弟節點並返回至performUnitOfWork
。重新執行beginWork到completeWork的整個render階段。
那麼completeWork做了什麼?這裡是completeWork的基本邏輯框架(我把bubbleProperties提出來方便理解每個completeWork
都會執行這前後兩條語句),做了popTreeContext
和bubbleProperties
。
function completeWork(current, workInProgress, renderLanes) {
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case FunctionComponent:
...
case HostComponent:
...
...
}
bubbleProperties(workInProgress);
}
popTreeContext是和上邊beginWork相關的內容,這裡的目的是使得正在進行的工作不處於堆棧頂部。對應pushContext的階段一般在beginWork的swtich中進入的函數中都可以找到
而bubbleProperties
的核心邏輯我也提了出來,可以看到這裡是做了一個層遍歷,遍歷了completedWorkFiber
的所有child,將它們的return賦值為completedWorkFiber
。同時,這裡也涉及了subtreeFlags
的計算,會將子節點的操作依據冒泡到父節點。
而關於subtreeFlags
的具體用處,在commit階段,我們後邊說。
function bubbleProperties(){
...
var newChildLanes = NoLanes;
var subtreeFlags = NoFlags;
{
var _child = completedWork.child;
while (_child !== null) {
newChildLanes = mergeLanes(newChildLanes, mergeLanes(_child.lanes, _child.childLanes));
subtreeFlags |= _child.subtreeFlags;
subtreeFlags |= _child.flags;
_child.return = completedWork;
_child = _child.sibling;
}
}
completedWork.subtreeFlags |= subtreeFlags;
}
...
}
後續的話,會根據workInProgress.tag
來走不同的邏輯,我們這裡主要說HostComponent的邏輯,代表原生組件。
下邊是我提煉出來的核心邏輯,這裡同樣會區分update
和mount
。
function completeWork(current, workInProgress, renderLanes) {
popTreeContext(workInProgress);
switch (workInProgress.tag) {
...
case HostComponent:{
popHostContext(workInProgress);
var type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent$1(current, workInProgress, type, newProps);
...
} else {
...
var currentHostContext = getHostContext();
var rootContainerInstance = getRootHostContainer();
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
...
}
bubbleProperties(workInProgress);
return null;
}
...
}
}
#1 update時
update時,無需生成新的DOM節點,所以此時要處理props,在updateHostComponent
中,第二部分會調用prepareUpdate->diffProperties
獲得一個updatePayload掛載在workInProgress.updateQueue
上
具體會處理哪些props,我們深入到diffProperties
就可以找到這一塊的邏輯
OK,那麼我們回到上邊所說的updatePayload
,調試發現updatePayload
是一個數組,數據結構體現為一個偶數為key,奇數為value的數組:
到了這一步,update流程最後會走入markUpdate
,至此。completeWork的update邏輯完畢
#2 mount時
我們此時來看mount時的邏輯,這裡最核心的邏輯簡化後其實只有幾句
function completeWork(current, workInProgress, renderLanes) {
popTreeContext(workInProgress);
...
var currentHostContext = getHostContext();
var rootContainerInstance = getRootHostContainer(); // 獲得root真實DOM
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);// 創建Fiber對應的真實DOM
appendAllChildren(instance, workInProgress, false, false);//將創建的真實dom插入workInProgressFiber
workInProgress.stateNode = instance;
...
bubbleProperties(workInProgress);
}
我們關註appendAllChildren
,這裡的邏輯是將新建的instance作為真實節點parent,將其插入到workInProgressFiber的真實節點中(因為一個Fiber節點不一定有真實節點,所以要找到可以插入的真實節點)
appendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
var node = workInProgress.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
} else if (node.tag === HostPortal) ; else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
};
那麼這裡實際做的就是把真實DOM掛載到workInProgressFiber
上,又由於我們上邊說了,complateWork是一個從子節點向上遍歷的過程,那麼遍歷完畢的時候,我們就得到了一顆構建好的workInProgress Tree
那麼接著,就是commit階段了。
4.4 commit階段
首先我們要知道commit階段的職責是什麼。
這樣的話,我們又要強調一下雙緩存樹了,workInProgress
樹是一顆在記憶體中構建的DOM樹,current
樹則是頁面正在渲染的DOM樹。
在此基礎上,render階段已經完成了記憶體中構建下一狀態的workInProgress
,那麼此時commit階段正應該做將current
樹與workInProgress
樹調換的工作。
而調換工作中,由於render階段的真實DOM並沒有更新,只是做了標記,此時會需要commit階段負責把這些更新根據不同的操作標記在真實DOM上操作。
commit階段開始於commitRoot
,往下就是調用commitRootImpl
,我們會著重分析commitRootImpl
首先看入參,可以看到commitRootImpl
的入參有四個,其中root
為最基本的參數,傳入的是已準備就緒的workInProgressRootFiber
。
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
renderPriorityLevel: EventPriority,
)
我們認為commit階段可以分為三個階段,分別代表
- before mutation,在執行DOM操作前的階段
- mutation,執行DOM操作
- layout,執行DOM操作之後
當然,在這些流程之外,commit階段還會處理useEffect
這類需要在commit階段執行的hook。
4.4.1 Before commit start
在commit開始之前,即before mutation之前的代碼可以從下邊看見,它們具體做了什麼我直接在代碼中註釋了,請看註釋。
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
renderPriorityLevel: EventPriority,
) {
do {
// 這裡會調度未執行完的useEffect,之所以上下各有一處,一方面是和React優先順序有關,一方面也和因為調度`useEffect`等hook時重新進入了render階段重新進入到commit階段有關。
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
...
// 和flags類似的二進位
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
// finishedWork是已經處理好的workInProgressRootFiber
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
...
if (finishedWork === null) {
return null;
}
//重置待commit的rootFiber,重置commit優先順序
root.finishedWork = null;
root.finishedLanes = NoLanes;
...
// commitRoot總是同步完成
// 所以在這裡清除Scheduler綁定的回調函數等變數允許綁定新的函數
root.callbackNode = null;
root.callbackPriority = NoLane;
//一些優先順序的計算
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);
markRootFinished(root, remainingLanes);
if (root === workInProgressRoot) {
// 完成後,重置全局變數
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
}
// 當finishedWork中存在PassiveMask標記時,調度useEffect
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
pendingPassiveTransitions = transitions;
scheduleCallback(NormalSchedulerPriority, () => {
// 這裡會調度useEffect的運行,詳情請看【useEffect】篇
flushPassiveEffects();
return null;
});
}
}
...
}
這裡有一點值得註意的是,伴隨著flushPassiveEffects
的調用,在堆棧中完全可能形成多次commit
,這是來源於useEffect
的副作用觸發了組件渲染,在這種情況下會再走一次狀態更新流程(當然這期間有優化)
4.4.2 BeforeMutation
commit階段的正式開始,在於commitBeforeMutationEffects
這個函數,可以看到當react確定subtreeFlags或者root.flags上可以找到BeforeMutationMask | MutationMask | LayoutMask | PassiveMask
時,會觸發commit的邏輯
var subtreeHasEffects = (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;
var rootHasEffect = (finishedWork.flags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;
if (subtreeHasEffects || rootHasEffect) {
...
var shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(root, finishedWork);
...
} else {
// No effects.
root.current = finishedWork;
}
那麼我們首先來看commitBeforeMutationEffects
,那麼可以看到commitBeforeMutationEffects緊接著調用了commitBeforeMutationEffects_begin
。
而commitBeforeMutationEffects_begin做的事情是從finishedWork
向下遍歷fiber樹,一直到遍歷到某個Fiber節點不再有BeforeMutationMask
標記,此時會進入commitBeforeMutationEffects_complete
。
function commitBeforeMutationEffects(root, firstChild) {
// 處理焦點相關的邏輯,處理原因是因為真實DOM的增刪導致可能出現的焦點變化
focusedInstanceHandle = prepareForCommit(root.containerInfo);
// nextEffect是一個全局變數,firstChild對應上方傳參`finishedWork`
nextEffect = firstChild;
commitBeforeMutationEffects_begin();
// 處理Blur相關的邏輯
var shouldFire = shouldFireAfterActiveInstanceBlur;
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;
return shouldFire;
}
function commitBeforeMutationEffects_begin() {
while (nextEffect !== null) {
var fiber = nextEffect;
var child = fiber.child;
if ((fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && child !== null) {
child.return = fiber;
nextEffect = child;
} else {
commitBeforeMutationEffects_complete();
}
}
}
而commitBeforeMutationEffects_complete
同樣是做了一次遍歷,這次的過程則是不斷向上返回,調用過程中不斷執行commitBeforeMutationEffectsOnFiber
。
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
var fiber = nextEffect;
setCurrentFiber(fiber);
try {
commitBeforeMutationEffectsOnFiber(fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentFiber();
var sibling = fiber.sibling;
if (sibling !== null) {
// 註意這裡,發現了嘛,和completeWork非常相似的邏輯對吧
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
繼續到commitBeforeMutationEffectsOnFiber
,發現這裡只有兩個簡單的內容
- 一個是對於ClassComponent會調用getSnapshotBeforeUpdate
- 另一個則是會HostRoot進行
clearContainer(root.containerInfo)
# 小結
那麼我們對BeforeMutation階段進行小結,現在我們知道React在BeforeMutation主要做了兩件事
- 處理真實DOM增刪後的
focus
、blur
邏輯 - 調用ClassComponent的
getSnapshotBeforeUpdate
生命周期鉤子
4.4.3 Mutation
commit第二階段,我們會進入commitMutationEffects
->commitMutationEffectsOnFiber
if (subtreeHasEffects || rootHasEffect) {
...
commitMutationEffects(root, finishedWork, lanes);
...
} else {
// No effects.
root.current = finishedWork;
}
commitMutationEffectsOnFiber
是一個368行的函數,它會根據Fiber.tag
和Fiber.flags
走不同的Mutation邏輯
目前來說,除了ScopeComponent
外的所有Component類型都會執行
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
所以我們首先走入recursivelyTraverseMutationEffects
,可以看到recursivelyTraverseMutationEffects
主要分成兩部分。
上邊的部分負責從Fiber.deletions
中取出具體的deletions
執行commitDeletionEffects
,後邊則是向下遍歷節點遞歸執行commitMutationEffectsOnFiber
。
function recursivelyTraverseMutationEffects(root, parentFiber, lanes) {
// Deletions effects can be scheduled on any fiber type. They need to happen
// before the children effects hae fired.
var deletions = parentFiber.deletions;
if (deletions !== null) {
for (var i = 0; i < deletions.length; i++) {
var childToDelete = deletions[i];
try {
commitDeletionEffects(root, parentFiber, childToDelete);
} catch (error) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
}
var prevDebugFiber = getCurrentFiber();
if (parentFiber.subtreeFlags & MutationMask) {
var child = parentFiber.child;
while (child !== null) {
setCurrentFiber(child);
commitMutationEffectsOnFiber(child, root);
child = child.sibling;
}
}
setCurrentFiber(prevDebugFiber);
}
我通覽這部分涉及的flags,發現會執行以下內容:
- Update->Insertion:執行React18推出的新hook,
useInsertionEffect
,會包含destory
和create
兩個階段
-
Update->Layout:執行
useLayoutEffect
上一次執行殘留的destory
函數 -
Placement: