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語法首先會(huì)被編譯成:
React.createElement("App", null) or jsx("App", null)
這里不詳說編譯方法,感興趣的可以參考:
babel在線編譯
新的jsx轉(zhuǎn)換
jsx語法轉(zhuǎn)換后,會(huì)通過creatElement
或jsx
的api轉(zhuǎn)換為React element
作為ReactDom.render()
的第一個(gè)參數(shù)進(jìn)行渲染。
在上一篇文章Fiber
中,我們提到過一個(gè)React項(xiàng)目會(huì)有一個(gè)fiberRoot
和一個(gè)或多個(gè)rootFiber
。fiberRoot
是一個(gè)項(xiàng)目的根節(jié)點(diǎn)。我們?cè)陂_始真正的渲染前會(huì)先基于root
DOM創(chuàng)建fiberRoot
,且fiberRoot.current = rootFiber
,這里的rootFiber
就是current
fiber樹的根節(jié)點(diǎn)。
if (!root) { // Initial mount root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate); fiberRoot = root._internalRoot; }
在創(chuàng)建好fiberRoot
和rootFiber
后,我們還不知道接下來要做什么,因?yàn)樗鼈兒臀覀兊?code><App />函數(shù)組件沒有一點(diǎn)關(guān)聯(lián)。這時(shí)React開始創(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è)更新:
上一篇文章也講過,React最多會(huì)同時(shí)擁有兩個(gè)fiber
樹,一個(gè)是current
fiber樹,另一個(gè)是workInProgress
fiber樹。current
fiber樹的根節(jié)點(diǎn)在上面已經(jīng)創(chuàng)建,下面會(huì)通過拷貝fiberRoot.current
的形式創(chuàng)建workInProgress
fiber樹的根節(jié)點(diǎn)。
到這里,前面的準(zhǔn)備工作就做完了, 接下來進(jìn)入正菜,開始進(jìn)行循環(huán)遍歷,生成fiber
樹和dom
樹,并最終渲染到頁面中。相關(guān)參考視頻講解:進(jìn)入學(xué)習(xí)
render階段
這個(gè)階段并不是指把代碼渲染到頁面上,而是基于我們的代碼畫出對(duì)應(yīng)的fiber
樹和dom
樹。
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
樹。
基于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)沒有了child
,這時(shí)beginWork
的工作就暫時(shí)告一段落,為什么說是暫時(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
樹。
基于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
生成的不完全的workInProgress
fiber樹向上查找,直到fiberRoot
。在這個(gè)向上的過程中,會(huì)去判斷是否有sibling
,如果有會(huì)再次走beginWork
,沒有就繼續(xù)向上。這樣到了根節(jié)點(diǎn),一個(gè)完整的dom
樹就生成了。
額外提一下,在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
中生成的workInProgress
fiber樹賦值給fiberRoot
的finishedWork
屬性。
var finishedWork = root.current.alternate; // workInProgress fiber樹 root.finishedWork = finishedWork; // 這里的root是fiberRoot root.finishedLanes = lanes; commitRoot(root);
在上面我們提到,如果一個(gè)fiber
節(jié)點(diǎn)有副作用會(huì)被記錄到父級(jí)fiber
的lastEffect
的nextEffect
。
在下面代碼中,如果fiber
樹有副作用,會(huì)將rootFiber.firstEffect
節(jié)點(diǎn)作為第一個(gè)副作用firstEffect
,并且將effectList
形成閉環(huán)。
var firstEffect; // 判斷當(dāng)前rootFiber樹是否有副作用 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樹沒有副作用 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
樹插入到root
DOM節(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中 } }
到這里,代碼終于真正的渲染到了頁面上。下面的commitWork
方法是執(zhí)行和useLayoutEffect()
有關(guān)的東西,這里不做重點(diǎn),后面文章安排,我們只要知道這里是執(zhí)行上一次更新的effect unmount
。
fiber樹切換
在講layout
階段之前,先來看下這行代碼
root.current = finishedWork // 將`workInProgress`fiber樹變成`current`樹
這行代碼在mutation和layout階段之間。在mutation階段, 此時(shí)的current
fiber樹還是指向更新前的fiber
樹, 這樣在生命周期鉤子內(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);
來看下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è)問題:
- 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問題
這篇文章主要介紹了React?createRef循環(huán)動(dòng)態(tài)賦值ref問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01forwardRef?中React父組件控制子組件的實(shí)現(xiàn)代碼
forwardRef 用于拿到父組件傳入的 ref 屬性,這樣在父組件便能通過 ref 控制子組件,這篇文章主要介紹了forwardRef?-?React父組件控制子組件的實(shí)現(xiàn)代碼,需要的朋友可以參考下2024-01-01react-router實(shí)現(xiàn)跳轉(zhuǎn)傳值的方法示例
這篇文章主要給大家介紹了關(guān)于react-router實(shí)現(xiàn)跳轉(zhuǎn)傳值的相關(guān)資料,文中給出了詳細(xì)的示例代碼,對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編一起來學(xué)習(xí)學(xué)習(xí)吧。2017-05-05react項(xiàng)目實(shí)踐之webpack-dev-serve
這篇文章主要介紹了react項(xiàng)目實(shí)踐之webpack-dev-serve,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-09-09create-react-app使用antd按需加載的樣式無效問題的解決
這篇文章主要介紹了create-react-app使用antd按需加載的樣式無效問題的解決,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-02-02詳解在React中跨組件分發(fā)狀態(tài)的三種方法
這篇文章主要介紹了詳解在React中跨組件分發(fā)狀態(tài)的三種方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08react-native ListView下拉刷新上拉加載實(shí)現(xiàn)代碼
本篇文章主要介紹了react-native ListView下拉刷新上拉加載實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08