React渲染機(jī)制超詳細(xì)講解
準(zhǔn)備工作
為了方便講解,假設(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項(xiàng)目中,這種jsx語(yǔ)法首先會(huì)被編譯成:
React.createElement("App", null)
or
jsx("App", null)這里不詳說(shuō)編譯方法,感興趣的可以參考:
babel在線編譯
新的jsx轉(zhuǎn)換
jsx語(yǔ)法轉(zhuǎn)換后,會(huì)通過(guò)creatElement或jsx的api轉(zhuǎn)換為React element作為ReactDom.render()的第一個(gè)參數(shù)進(jìn)行渲染。
在上一篇文章Fiber中,我們提到過(guò)一個(gè)React項(xiàng)目會(huì)有一個(gè)fiberRoot和一個(gè)或多個(gè)rootFiber。fiberRoot是一個(gè)項(xiàng)目的根節(jié)點(diǎn)。我們?cè)陂_(kāi)始真正的渲染前會(huì)先基于rootDOM創(chuàng)建fiberRoot,且fiberRoot.current = rootFiber,這里的rootFiber就是currentfiber樹(shù)的根節(jié)點(diǎn)。
if (!root) {
// Initial mount
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
fiberRoot = root._internalRoot;
}在創(chuàng)建好fiberRoot和rootFiber后,我們還不知道接下來(lái)要做什么,因?yàn)樗鼈兒臀覀兊?code><App />函數(shù)組件沒(méi)有一點(diǎn)關(guān)聯(lián)。這時(shí)React開(kāi)始創(chuàng)建update,并將ReactDom.render()的第一個(gè)參數(shù),也就是基于<App />創(chuàng)建的React element賦給update。
var update = {
eventTime: eventTime,
lane: lane,
tag: UpdateState,
payload: null,
callback: element,
next: null
};有了這個(gè)update,還需要將它加入到更新隊(duì)列中,等待后續(xù)進(jìn)行更新。在這里有必要講下這個(gè)隊(duì)列的創(chuàng)建流程,這個(gè)創(chuàng)建操作在React有多次應(yīng)用。
var sharedQueue = updateQueue.shared;
var pending = sharedQueue.pending;
if (pending === null) {
// mount時(shí)只有一個(gè)update,直接閉環(huán)
update.next = update;
} else {
// update時(shí),將最新的update的next指向上一次的update, 上一次的update的next又指向最新的update形成閉環(huán)
update.next = pending.next;
pending.next = update;
}
// pending指向最新的update, 這樣我們遍歷update鏈表時(shí), pending.next會(huì)指向第一個(gè)插入的update。
sharedQueue.pending = update; 我將上面的代碼進(jìn)行了一下抽象,更新隊(duì)列是一個(gè)環(huán)形鏈表結(jié)構(gòu),每次向鏈表結(jié)尾添加一個(gè)update時(shí),指針都會(huì)指向這個(gè)update,并且這個(gè)update.next會(huì)指向第一個(gè)更新:

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

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

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

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

