5渲染判断if_React 16 渲染流程
學過微機的同學都應該很熟悉「中斷」這個概念:
- CPU 正常運行程序時,內部事件或外設提出中斷請求;
- CPU 予以響應,同時保護好 CPU 執行主程序的現場,轉入調用中斷服務程序;
- 調用完畢后恢復現場。
本文學習的 React 源碼版本:16.9.0
Fiber 調度/渲染
Stack Reconciler
React 16 之前的組件渲染方式是遞歸渲染:渲染父節點 -> 渲染子節點
遞歸渲染看起來十分簡單,但是如果想在子節點的渲染過程中執行優先級更高的操作,只能保留調用棧中子節點的渲染及子節點之前節點的渲染,這樣是很復雜的,這種調和/渲染也叫做 Stack Reconciler。
Fiber Reconciler
Fiber 使用鏈表的結構去渲染節點,每一個節點都稱之為 Fiber Node,每個節點會有三個屬性:
- child 指向第一個子節點
- sibling 指向兄弟節點
- return 指向父節點
Fiber 的渲染方式:從父節點開始,向下依次遍歷子節點,深度優先渲染完子節點后,再回到其父節點去檢查是否有兄弟節點,如果有兄弟節點,則從該兄弟節點開始繼續深度優先的渲染,直到回退到根節點結束。
重復遍歷的節點并不會重復渲染,而是為了取到下一個可能需要渲染的節點。
此時每一個節點都是一個渲染任務, 從而將整個界面渲染任務拆分成更小的模塊,渲染可拆分就意味著每次任務執行前都可以檢查是否去執行優先級更高的操作。
Fiber Node Tree
實際上,真實的 DOM 渲染過程是 diff 兩棵 Fiber 節點樹得到 effect list,在 commit 階段執行。在 React16 中,兩棵樹分別是:
- current tree (在源碼中即 HostRoot.current)
- workInProgress tree(在源碼中即 HostRoot.current.alternate)
不過 React 并沒有實現兩棵 Fiber Node Tree,實際情況是兩棵樹上對應的 Fiber Node 通過 alternate 屬性互相引用。
// packages/react-reconiler/src/ReactFiber.js // This is used to create an alternate fiber to do work on. export function createWorkInProgress() {// ...workInProgress.pendingProps = pendingProps;workInProgress.child = current.child;workInProgress.sibling = current.sibling;// ... }React 渲染流程
可以分為 Scheduler、Reconciliation、Commit 這三個階段
Scheduler 階段
Scheduer 流程主要是創建更新,創建更新的方式:
- ReactDOM.render
- setState
可以發現 React 將首次渲染和更新渲染統一了起來。
ReactDOM.render
調用 legacyRenderSubtreeIntoContainer
// packages/react-dom/src/client/ReactDOM.js const ReactDOM = {render() {// ...return legacyRenderSubtreeIntoContainer(null,element,container,false,callback,);} }legacyRenderSubtreeIntoContainer
調用 root.render,root 來自調用 legacyCreateRootFromDOMContainer。
// packages/react-dom/src/client/ReactDOM.js function legacyRenderSubtreeIntoContainer() {let root: _ReactSyncRoot = (container._reactRootContainer: any);if (!root) {// Initial mountroot = container._reactRootContainer = legacyCreateRootFromDOMContainer(container,forceHydrate,);} else {// UpdateupdateContainer(children, fiberRoot, parentComponent, callback);} }legacyCreateRootFromDOMContainer
- 清除根節點下的所有子元素
- 創建 ReactRoot
ReactSyncRoot
后面就是創建 FiberRoot 了,不放源碼上來了。
setState
enqueueUpdate
將當前的更新壓入更新隊列
// packages/src/react-reconciler/src/ReactFiberClassComponent.js const updater = {enqueueUpdate(inst, payload, callback) {const fiber = getInstance(inst);const currentTime = requestCurrentTime();const suspenseConfig = requestCurrentSuspenseConfig();// 到期時間(Fiber 中的優先級)const expirationTime = computeExpirationForFiber(currentTime,fiber,suspenseConfig,);const update = createUpdate(expirationTime, suspenseConfig);update.payload = payload; // payload 為 setState 中的更新對象if (callback !== undefined && callback !== null) {if (__DEV__) {warnOnInvalidCallback(callback, 'setState');}update.callback = callback;}enqueueUpdate(fiber, update); // 更新信息放入 updateQueuescheduleWork(fiber, expirationTime); // 并行渲染的核心:Scheduler} }scheduleWork
// packages/react-reconciler/src/ReactFiberWOrkLoop.js function scheduleUpdateOnFiber() {// ... next }scheduler 的具體過程會在之后的并行渲染中展開。
Reconciliation 階段
workLoop
循環更新,對整棵 Fiber 樹都遍歷一遍。
循環每渲染完成一個 Fiber Node 就利用 shouldYield 來判斷是否有優先級更高的任務存在,是則跳出循環先執行優先級更高的任務,否則繼續渲染下一個 Fiber Node。
還記得文章一開始介紹了微機中的中斷概念么,可以看到在 workLoop 過程中體現出來了。
簡單來說就是判斷當前幀是否還有時間更新,如果沒有時間更新就將剩余時間去進行其他操作。
// packages/react-reconciler/src/ReactFiberWorkLoop.js function renderRoot() {do {try {if (isSync) {workLoopSync();} else {workLoop(); }break;}// ... }function workLoop() {while (workInProgress !== null && !shouldYield()) {workInProgress = performUnitOfWork(workInProgress);} }performUnitOfWork
調用 beginWork 更新當前任務節點,如果 Fiber 樹已經更新到葉子節點,則調用 completeUnitOfWork 更新。
// packages/react-reconciler/src/ReactFiberWorkLoop.js function performUnitOfWork(unitOfWork: Fiber): Fiber | null {// 調和階段都是在 alternate 上完成const current = unitOfWork.alternate;// ...next = beginWork(current, unitOfWork, renderExpirationTime);unitOfWork.memoizedProps = unitOfWork.pendingProps;// Fiber 樹已經更新到葉子節點if (next === null) {next = completeUnitOfWork(unitOfWork);}return next; }beginWork
根據 workInProgress 的 tag ,把對應的 FiberNode 上下文壓入棧,然后更新節點,對應 render 階段。
// packages/react-reconciler/src/ReactFiberBeginWork.js function beginWork() {...switch (workInProgress.tag) {case ClassComponent: {const Component = workInProgress.type;if (isLegacyContextProvider(Component)) {pushLegacyContextProvider(workInProgress); // 入棧}break;}} }beginWork 會返回當前節點的子節點,如果有子節點,繼續 workLoop;如果沒有子節點,進入 completeUnitOfWork
子節點的 alternate 改變是在 cloneChildFibers 函數中
completeWork
改變 effectList(firstEffect、lastEffect、nextEffect)
作用是將 Fiber Node 上下文出棧,對應 commit 階段
// packages/react-reconciler/ReactFiberCompleteWork.js function completeWork() {const newProps = workInProgress.pendingProps;switch (workInProgress.tag) {case ClassComponent: {const Component = workInProgress.type;if (isLegacyContextProvider(Component)) {popLegacyContext(workInProgress); // 出棧}break;}} }commit 階段
從字面意思來就可以知道, commit 階段是將調和階段的更新進行提交,即把更新操作反映到真實的 DOM 上。
同時,commit 階段是同步執行,不可被中斷。
Effect
函數式編程中經常會看見 Effect 這個概念,表示副作用。在 Fiber 架構中,Effect 定義了 Fiber Node 在 commit 階段要做的事情,在源碼中也就是 EffectTag 這個屬性。
- 對于組件:更新 refs、調用 componentDidUpdate...
- 對于 DOM:增加、更新、刪除 DOM...
Effect 組成的鏈表成為 effects list
- firstEffect:指向第一個更新的節點
- nextEffect:指向下一個更新的節點
commitRoot
使 effects list 生效:
- 第一次遍歷 effects list(commitBeforeMutationEffects):在更改前讀取 DOM 上的 state,這里是 getSnapshotBeforeUpdate 生命周期調用的地方;
- 第二次遍歷 effects list(commitMutationEffects):此階段是真正更改 DOM 的階段;
- 第三次遍歷 effects list(commitLayoutEffects):執行生命周期函數 componentDidMount、componentDidUpdate...
commitBeforeMutationEffects
- 通過 prevProps、prevState 以獲取 Snapshot;
- 調用組件實例的 getSnapshotBeforeUpdate,返回值用于 componentDidUpdate 的第三個參數。
commitMutationEffects
根據不同的 effectTag 執行不同的操作:
- 插入節點:commitPlacement
- 更新節點:commitWork
- 刪除節點:commitDeletion
commitPlacement -- 插入節點
- 找到 finishedWork 的父節點 parentFiber。尋找的是原生的 DOM 節點對應的 Fiber Node,如果父級不是原生 DOM,則繼續往上尋找。
- 找到待插入節點的后一個節點
- 使用 insertBefore 或 appendChild 或深度優先遍歷 class 組件的子節點插入
commitWork -- 更新節點
commitWork 只會對 HostComponent 和 HostText 進行更新,也就是 DOM 節點和文本節點。
- HostComponent 調用 commitUpdate
- HostText 調用 commitTextUpdate
commitUpdate
updatePayload 應用到真實 DOM 上;對一些屬性做特殊處理
// packages/react-dom/src/client/ReactDOMHostConfig.js function commitUpdate() {updateFiberProps(domElement, newProps);updateProperties(domElement, updatePayload, type, oldProps, newProps); }// packages/react-dom/src/client/ReactDOMComponent.js function updateDOMProperties(domElement: Element,updatePayload: Array<any>,wasCustomComponentTag: boolean,isCustomComponentTag: boolean, ): void {for (let i = 0; i < updatePayload.length; i += 2) {const propKey = updatePayload[i];const propValue = updatePayload[i + 1];if (propKey === STYLE) {setValueForStyles(domElement, propValue);} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {setInnerHTML(domElement, propValue);} else if (propKey === CHILDREN) {setTextContent(domElement, propValue);} else {setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);}} }commitTextUpdate
文本更新很簡單,直接替換 value
// packages/react-dom/src/client/ReactDOMHostConfig.js function commitTextUpdate() {textInstance.nodeValue = newText; }commitDeletion -- 刪除節點
刪除節點也需要考慮到子節點不一定是原生 DOM 的情況,比如如果是 class Component,需要調用 componentWillUnmount,所以還是需要深度遍歷整個子樹
// packages/react-reconciler/src/ReactFiberCommitWork.jsfunction commitDeletion(current: Fiber,renderPriorityLevel: ReactPriorityLevel, ): void {if (supportsMutation) {unmountHostComponents(current, renderPriorityLevel);} else {// Detach refs and call componentWillUnmount() on the whole subtree.commitNestedUnmounts(current, renderPriorityLevel);}detachFiber(current); }commitLayoutEffects
- 執行 componentDidMount、componentDidUpdate
參考
- 探究 React Work Loop 原理
總結
以上是生活随笔為你收集整理的5渲染判断if_React 16 渲染流程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 屏幕自动亮度不停的变_LCD最后的荣耀?
- 下一篇: 邵兴有几个公墓地