React渲染機制超詳細講解
準備工作
為了方便講解,假設(shè)我們有下面這樣一段代碼:
function App(){
const [count, setCount] = useState(0)
useEffect(() => {
setCount(1)
}, [])
const handleClick = () => setCount(count => count++)
return (
<div>
勇敢牛牛, <span>不怕困難</span>
<span onClick={handleClick}>{count}</span>
</div>
)
}
ReactDom.render(<App />, document.querySelector('#root'))在React項目中,這種jsx語法首先會被編譯成:
React.createElement("App", null)
or
jsx("App", null)這里不詳說編譯方法,感興趣的可以參考:
babel在線編譯
新的jsx轉(zhuǎn)換
jsx語法轉(zhuǎn)換后,會通過creatElement或jsx的api轉(zhuǎn)換為React element作為ReactDom.render()的第一個參數(shù)進行渲染。
在上一篇文章Fiber中,我們提到過一個React項目會有一個fiberRoot和一個或多個rootFiber。fiberRoot是一個項目的根節(jié)點。我們在開始真正的渲染前會先基于rootDOM創(chuàng)建fiberRoot,且fiberRoot.current = rootFiber,這里的rootFiber就是currentfiber樹的根節(jié)點。
if (!root) {
// Initial mount
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
fiberRoot = root._internalRoot;
}在創(chuàng)建好fiberRoot和rootFiber后,我們還不知道接下來要做什么,因為它們和我們的<App />函數(shù)組件沒有一點關(guān)聯(lián)。這時React開始創(chuàng)建update,并將ReactDom.render()的第一個參數(shù),也就是基于<App />創(chuàng)建的React element賦給update。
var update = {
eventTime: eventTime,
lane: lane,
tag: UpdateState,
payload: null,
callback: element,
next: null
};有了這個update,還需要將它加入到更新隊列中,等待后續(xù)進行更新。在這里有必要講下這個隊列的創(chuàng)建流程,這個創(chuàng)建操作在React有多次應用。
var sharedQueue = updateQueue.shared;
var pending = sharedQueue.pending;
if (pending === null) {
// mount時只有一個update,直接閉環(huán)
update.next = update;
} else {
// update時,將最新的update的next指向上一次的update, 上一次的update的next又指向最新的update形成閉環(huán)
update.next = pending.next;
pending.next = update;
}
// pending指向最新的update, 這樣我們遍歷update鏈表時, pending.next會指向第一個插入的update。
sharedQueue.pending = update; 我將上面的代碼進行了一下抽象,更新隊列是一個環(huán)形鏈表結(jié)構(gòu),每次向鏈表結(jié)尾添加一個update時,指針都會指向這個update,并且這個update.next會指向第一個更新:

上一篇文章也講過,React最多會同時擁有兩個fiber樹,一個是currentfiber樹,另一個是workInProgressfiber樹。currentfiber樹的根節(jié)點在上面已經(jīng)創(chuàng)建,下面會通過拷貝fiberRoot.current的形式創(chuàng)建workInProgressfiber樹的根節(jié)點。
到這里,前面的準備工作就做完了, 接下來進入正菜,開始進行循環(huán)遍歷,生成fiber樹和dom樹,并最終渲染到頁面中。相關(guān)參考視頻講解:進入學習
render階段
這個階段并不是指把代碼渲染到頁面上,而是基于我們的代碼畫出對應的fiber樹和dom樹。
workloopSync
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}在這個循環(huán)里,會不斷根據(jù)workInProgress找到對應的child作為下次循環(huán)的workInProgress,直到遍歷到葉子節(jié)點,即深度優(yōu)先遍歷。在performUnitOfWork會執(zhí)行下面的beginWork。

beginWork
簡單描述下beginWork的工作,就是生成fiber樹。
基于workInProgress的根節(jié)點生成<App />的fiber節(jié)點并將這個節(jié)點作為根節(jié)點的child,然后基于<App />的fiber節(jié)點生成<div />的fiber節(jié)點并作為<App />的fiber節(jié)點的child,如此循環(huán)直到最下面的牛牛文本。

注意, 在上面流程圖中,updateFunctionComponent會執(zhí)行一個renderWithHooks函數(shù),這個函數(shù)里面會執(zhí)行App()這個函數(shù)組件,在這里會初始化函數(shù)組件里所有的hooks,也就是上面實例代碼的useState()。
當遍歷到牛牛文本時,它的下面已經(jīng)沒有了child,這時beginWork的工作就暫時告一段落,為什么說是暫時,是因為在completeWork時,如果遍歷的fiber節(jié)點有sibling會再次走到beginWork。
completeWork
當遍歷到牛牛文本后,會進入這個completeWork。
在這里,我們再簡單描述下completeWork的工作, 就是生成dom樹。
基于fiber節(jié)點生成對應的dom節(jié)點,并且將這個dom節(jié)點作為父節(jié)點,將之前生成的dom節(jié)點插入到當前創(chuàng)建的dom節(jié)點。并會基于在beginWork生成的不完全的workInProgressfiber樹向上查找,直到fiberRoot。在這個向上的過程中,會去判斷是否有sibling,如果有會再次走beginWork,沒有就繼續(xù)向上。這樣到了根節(jié)點,一個完整的dom樹就生成了。

額外提一下,在completeWork中有這樣一段代碼
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}解釋一下, flags > PerformedWork代表當前這個fiber節(jié)點是有副作用的,需要將這個fiber節(jié)點加入到父級fiber的effectList鏈表中。
commit階段
這個階段的主要工作是處理副作用。所謂副作用就是不確定操作,比如:插入,替換,刪除DOM,還有useEffect()hook的回調(diào)函數(shù)都會被作為副作用。
commitWork
準備工作
在commitWork前,會將在workloopSync中生成的workInProgressfiber樹賦值給fiberRoot的finishedWork屬性。
var finishedWork = root.current.alternate; // workInProgress fiber樹 root.finishedWork = finishedWork; // 這里的root是fiberRoot root.finishedLanes = lanes; commitRoot(root);
在上面我們提到,如果一個fiber節(jié)點有副作用會被記錄到父級fiber的lastEffect的nextEffect。
在下面代碼中,如果fiber樹有副作用,會將rootFiber.firstEffect節(jié)點作為第一個副作用firstEffect,并且將effectList形成閉環(huán)。
var firstEffect;
// 判斷當前rootFiber樹是否有副作用
if (finishedWork.flags > PerformedWork) {
// 下面代碼的目的還是為了將這個effectList鏈表形成閉環(huán)
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// 這個rootFiber樹沒有副作用
firstEffect = finishedWork.firstEffect;
}mutation之前
簡單描述mutation之前階段的工作:
處理DOM節(jié)點渲染/刪除后的 autoFocus、blur 邏輯;
調(diào)用getSnapshotBeforeUpdate,fiberRoot和ClassComponent會走這里;
調(diào)度useEffect(異步);
在mutation之前的階段,遍歷effectList鏈表,執(zhí)行commitBeforeMutationEffects方法。
do { // mutation之前
invokeGuardedCallback(null, commitBeforeMutationEffects, null);
} while (nextEffect !== null);我們進到commitBeforeMutationEffects方法,我將代碼簡化一下:
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
var current = nextEffect.alternate;
// 處理DOM節(jié)點渲染/刪除后的 autoFocus、blur 邏輯;
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null){...}
var flags = nextEffect.flags;
// 調(diào)用getSnapshotBeforeUpdate,fiberRoot和ClassComponent會走這里
if ((flags & Snapshot) !== NoFlags) {...}
// 調(diào)度useEffect(異步)
if ((flags & Passive) !== NoFlags) {
// rootDoesHavePassiveEffects變量表示當前是否有副作用
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
// 創(chuàng)建任務并加入任務隊列,會在layout階段之后觸發(fā)
scheduleCallback(NormalPriority$1, function () {
flushPassiveEffects();
return null;
});
}
}
// 繼續(xù)遍歷下一個effect
nextEffect = nextEffect.nextEffect;
}
}按照我們示例代碼,我們重點關(guān)注第三件事,調(diào)度useEffect(注意,這里是調(diào)度,并不會馬上執(zhí)行)。
scheduleCallback主要工作是創(chuàng)建一個task:
var newTask = {
id: taskIdCounter++,
callback: callback, //上面代碼傳入的回調(diào)函數(shù)
priorityLevel: priorityLevel,
startTime: startTime,
expirationTime: expirationTime,
sortIndex: -1
};它里面有個邏輯會判斷startTime和currentTime, 如果startTime > currentTime,會把這個任務加入到定時任務隊列timerQueue,反之會加入任務隊列taskQueue,并task.sortIndex = expirationTime。
mutation
簡單描述mutation階段的工作就是負責dom渲染。
區(qū)分fiber.flags,進行不同的操作,比如:重置文本,重置ref,插入,替換,刪除dom節(jié)點。
和mutation之前階段一樣,也是遍歷effectList鏈表,執(zhí)行commitMutationEffects方法。
do { // mutation dom渲染
invokeGuardedCallback(null, commitMutationEffects, null, root, renderPriorityLevel);
} while (nextEffect !== null);看下commitMutationEffects的主要工作:
function commitMutationEffects(root, renderPriorityLevel) {
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) { // 遍歷EffectList
setCurrentFiber(nextEffect);
// 根據(jù)flags分別處理
var flags = nextEffect.flags;
// 根據(jù) ContentReset flags重置文字節(jié)點
if (flags & ContentReset) {...}
// 更新ref
if (flags & Ref) {...}
var primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
case Placement: // 插入dom
{...}
case PlacementAndUpdate: //插入dom并更新dom
{
// Placement
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement; // Update
var _current = nextEffect.alternate;
commitWork(_current, nextEffect);
break;
}
case Hydrating: //SSR
{...}
case HydratingAndUpdate: // SSR
{...}
case Update: // 更新dom
{...}
case Deletion: // 刪除dom
{...}
}
resetCurrentFiber();
nextEffect = nextEffect.nextEffect;
}
}按照我們的示例代碼,這里會走PlacementAndUpdate,首先是commitPlacement(nextEffect)方法,在一串判斷后,最后會把我們生成的dom樹插入到rootDOM節(jié)點中。
function appendChildToContainer(container, child) {
var parentNode;
if (container.nodeType === COMMENT_NODE) {
parentNode = container.parentNode;
parentNode.insertBefore(child, container);
} else {
parentNode = container;
parentNode.appendChild(child); // 直接將整個dom作為子節(jié)點插入到root中
}
}到這里,代碼終于真正的渲染到了頁面上。下面的commitWork方法是執(zhí)行和useLayoutEffect()有關(guān)的東西,這里不做重點,后面文章安排,我們只要知道這里是執(zhí)行上一次更新的effect unmount。
fiber樹切換
在講layout階段之前,先來看下這行代碼
root.current = finishedWork // 將`workInProgress`fiber樹變成`current`樹
這行代碼在mutation和layout階段之間。在mutation階段, 此時的currentfiber樹還是指向更新前的fiber樹, 這樣在生命周期鉤子內(nèi)獲取的DOM就是更新前的, 類似于componentDidMount和compentDidUpdate的鉤子是在layout階段執(zhí)行的,這樣就能獲取到更新后的DOM進行操作。
layout
簡單描述layout階段的工作:
- 調(diào)用生命周期或hooks相關(guān)操作
- 賦值ref
和mutation之前階段一樣,也是遍歷effectList鏈表,執(zhí)行commitLayoutEffects方法。
do { // 調(diào)用生命周期和hook相關(guān)操作, 賦值ref
invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
} while (nextEffect !== null);來看下commitLayoutEffects方法:
function commitLayoutEffects(root, committedLanes) {
while (nextEffect !== null) {
setCurrentFiber(nextEffect);
var flags = nextEffect.flags;
// 調(diào)用生命周期或鉤子函數(shù)
if (flags & (Update | Callback)) {
var current = nextEffect.alternate;
commitLifeCycles(root, current, nextEffect);
}
{
// 獲取dom實例,更新ref
if (flags & Ref) {
commitAttachRef(nextEffect);
}
}
resetCurrentFiber();
nextEffect = nextEffect.nextEffect;
}
}提一下,useLayoutEffect()的回調(diào)會在commitLifeCycles方法中執(zhí)行,而useEffect()的回調(diào)會在commitLifeCycles中的schedulePassiveEffects方法進行調(diào)度。從這里就可以看出useLayoutEffect()和useEffect()的區(qū)別:
useLayoutEffect的上次更新銷毀函數(shù)在mutation階段銷毀,本次更新回調(diào)函數(shù)是在dom渲染后的layout階段同步執(zhí)行;useEffect在mutation之前階段會創(chuàng)建調(diào)度任務,在layout階段會將銷毀函數(shù)和回調(diào)函數(shù)加入到pendingPassiveHookEffectsUnmount和pendingPassiveHookEffectsMount隊列中,最終它的上次更新銷毀函數(shù)和本次更新回調(diào)函數(shù)都是在layout階段后異步執(zhí)行; 可以明確一點,他們的更新都不會阻塞dom渲染。
layout之后
還記得在mutation之前階段的這幾行代碼嗎?
// 創(chuàng)建任務并加入任務隊列,會在layout階段之后觸發(fā)
scheduleCallback(NormalPriority$1, function () {
flushPassiveEffects();
return null;
});這里就是在調(diào)度useEffect(),在layout階段之后會執(zhí)行這個回調(diào)函數(shù),此時會處理useEffect的上次更新銷毀函數(shù)和本次更新回調(diào)函數(shù)。
總結(jié)
看完這篇文章, 我們可以弄明白下面這幾個問題:
- React的渲染流程是怎樣的?
- React的beginWork都做了什么?
- React的completeWork都做了什么?
- React的commitWork都做了什么?
- useEffect和useLayoutEffect的區(qū)別是什么?
- useEffect和useLayoutEffect的銷毀函數(shù)和更新回調(diào)的調(diào)用時機?
到此這篇關(guān)于React渲染機制超詳細講解的文章就介紹到這了,更多相關(guān)React渲染機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React?createRef循環(huán)動態(tài)賦值ref問題
這篇文章主要介紹了React?createRef循環(huán)動態(tài)賦值ref問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01
forwardRef?中React父組件控制子組件的實現(xiàn)代碼
forwardRef 用于拿到父組件傳入的 ref 屬性,這樣在父組件便能通過 ref 控制子組件,這篇文章主要介紹了forwardRef?-?React父組件控制子組件的實現(xiàn)代碼,需要的朋友可以參考下2024-01-01
react-router實現(xiàn)跳轉(zhuǎn)傳值的方法示例
這篇文章主要給大家介紹了關(guān)于react-router實現(xiàn)跳轉(zhuǎn)傳值的相關(guān)資料,文中給出了詳細的示例代碼,對大家具有一定的參考學習價值,需要的朋友們下面跟著小編一起來學習學習吧。2017-05-05
create-react-app使用antd按需加載的樣式無效問題的解決
這篇文章主要介紹了create-react-app使用antd按需加載的樣式無效問題的解決,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-02-02
詳解在React中跨組件分發(fā)狀態(tài)的三種方法
這篇文章主要介紹了詳解在React中跨組件分發(fā)狀態(tài)的三種方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08
react-native ListView下拉刷新上拉加載實現(xiàn)代碼
本篇文章主要介紹了react-native ListView下拉刷新上拉加載實現(xiàn),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08

