圖片做者:Artem Sapegin,來源: https://unsplash.com/photos/b...本文做者:劉鵬javascript
在 React v16.13 版本中,正式推出了實驗性的 Concurrent Mode,尤爲是提供一種新的機制 Suspense,很是天然地解決了一直以來存在的異步反作用問題。結合前面 v16.8 推出的 Hooks,v16.0 底層架構 Fiber,React 給開發者體驗上帶來了極大提高以及必定程度上更佳的用戶體驗。因此,對 React 17,你會有什麼期待?前端
咱們知道,Stack Reconciler 是 React v15 及以前版本使用的協調算法。而 React Fiber 則是從 v16 版本開始對 Stack Reconciler 進行的重寫,是 v16 版本的核心算法實現。
Stack Reconciler 的實現使用了同步遞歸模型,該模型依賴於內置堆棧來遍歷。React 團隊 Andrew 以前有提到:java
若是隻依賴內置調用堆棧,那麼它將一直工做,直到堆棧爲空,若是咱們能夠隨意中斷調用堆棧並手動操做堆棧幀,這不是很好嗎? 這就是 React Fiber 的目標。Fiber 是內置堆棧的從新實現,專門用於 React 組件,能夠將一個 fiber 看做是一個虛擬堆棧幀。
正是因爲其內置 Stack Reconciler 天生帶來的侷限性,使得 DOM 更新過程是同步的。也就是說,在虛擬 DOM 的比對過程當中,若是發現一個元素實例有更新,則會當即同步執行操做,提交到真實 DOM 的更改。這在動畫、佈局以及手勢等領域,可能會帶來很是糟糕的用戶體驗。所以,爲了解決這個問題,React 實現了一個虛擬堆棧幀。實際上,這個所謂的虛擬堆棧幀本質上是創建了多個包含節點和指針的鏈表數據結構。每個節點就是一個 fiber 基本單元,這個對象存儲了必定的組件相關的數據域信息。而指針的指向,則是串聯起整個 fibers 樹。從新自定義堆棧帶來顯而易見的優勢是,能夠將堆棧保留在內存中,在須要執行的時候執行它們,這使得暫停遍歷和中止堆棧遞歸成爲可能。node
Fiber 的主要目標是實現虛擬 DOM 的增量渲染,可以將渲染工做拆分紅塊並將其分散到多個幀的能力。在新的更新到來時,可以暫停、停止和複用工做,能爲不一樣類型的更新分配優先級順序的能力。理解 React 運行機制對咱們更好理解它的設計思想以及後續版本新增特性,好比 v17 版本可能帶來的異步渲染能力,相信會有很好的幫助。本文基於 React v16.8.6 版本源碼,輸出一些淺見,但願對你也有幫助,若有不對,還望指正。react
在瞭解 React Fiber 架構的實現機制以前,有必要先把幾個主要的基礎概念拋出來,以便於咱們更好地理解。git
在 React Reconciliation 過程當中出現的各類必須執行計算的活動,好比 state update,props update 或 refs update 等,這些活動咱們能夠統一稱之爲 work。github
文件位置:packages/react-reconciler/src/ReactFiber.js
每個 React 元素對應一個 fiber 對象,一個 fiber 對象一般是表徵 work 的一個基本單元。fiber 對象有幾個屬性,這些屬性指向其餘 fiber 對象。算法
所以 fibers 能夠理解爲是一個包含 React 元素上下文信息的數據域節點,以及由 child, sibling 和 return 等指針域構成的鏈表結構。安全
fiber 對象主要的屬性以下所示:數據結構
Fiber = { // 標識 fiber 類型的標籤,詳情參看下述 WorkTag tag: WorkTag, // 指向父節點 return: Fiber | null, // 指向子節點 child: Fiber | null, // 指向兄弟節點 sibling: Fiber | null, // 在開始執行時設置 props 值 pendingProps: any, // 在結束時設置的 props 值 memoizedProps: any, // 當前 state memoizedState: any, // Effect 類型,詳情查看如下 effectTag effectTag: SideEffectTag, // effect 節點指針,指向下一個 effect nextEffect: Fiber | null, // effect list 是單向鏈表,第一個 effect firstEffect: Fiber | null, // effect list 是單向鏈表,最後一個 effect lastEffect: Fiber | null, // work 的過時時間,可用於標識一個 work 優先級順序 expirationTime: ExpirationTime, };
從 React 元素建立一個 fiber 對象
文件位置:react-reconciler/src/ReactFiber.js
export function createFiberFromElement( element: ReactElement, mode: TypeOfMode, expirationTime: ExpirationTime ): Fiber { const fiber = createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, expirationTime); return fiber; }
文件位置:shared/ReactWorkTags.js
上述 fiber 對象的 tag 屬性值,稱做 workTag,用於標識一個 React 元素的類型,以下所示:
export const FunctionComponent = 0; export const ClassComponent = 1; export const IndeterminateComponent = 2; // Before we know whether it is function or class export const HostRoot = 3; // Root of a host tree. Could be nested inside another node. export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer. export const HostComponent = 5; export const HostText = 6; export const Fragment = 7; export const Mode = 8; export const ContextConsumer = 9; export const ContextProvider = 10; export const ForwardRef = 11; export const Profiler = 12; export const SuspenseComponent = 13; export const MemoComponent = 14; export const SimpleMemoComponent = 15; export const LazyComponent = 16; export const IncompleteClassComponent = 17; export const DehydratedSuspenseComponent = 18; export const EventComponent = 19; export const EventTarget = 20; export const SuspenseListComponent = 21;
文件位置:shared/ReactSideEffectTags.js
上述 fiber 對象的 effectTag 屬性值,每個 fiber 節點都有一個和它相關聯的 effectTag 值。
咱們把不能在 render 階段完成的一些 work 稱之爲反作用,React 羅列了可能存在的各種反作用,以下所示:
export const NoEffect = /* */ 0b000000000000; export const PerformedWork = /* */ 0b000000000001; export const Placement = /* */ 0b000000000010; export const Update = /* */ 0b000000000100; export const PlacementAndUpdate = /* */ 0b000000000110; export const Deletion = /* */ 0b000000001000; export const ContentReset = /* */ 0b000000010000; export const Callback = /* */ 0b000000100000; export const DidCapture = /* */ 0b000001000000; export const Ref = /* */ 0b000010000000; export const Snapshot = /* */ 0b000100000000; export const Passive = /* */ 0b001000000000; export const LifecycleEffectMask = /* */ 0b001110100100; export const HostEffectMask = /* */ 0b001111111111; export const Incomplete = /* */ 0b010000000000; export const ShouldCapture = /* */ 0b100000000000;
協調(Reconciliation):
簡而言之,根據 diff 算法來比較虛擬 DOM,從而能夠確認哪些部分的 React 元素須要更改。
調度(Scheduling):
能夠簡單理解爲是一個肯定在何時執行 work 的過程。
相信不少同窗都看過這張圖,這是 React 團隊做者 Dan Abramov 畫的一張生命週期階段圖,詳情點擊查看。他把 React 的生命週期主要分爲兩個階段:render 階段和 commit 階段。其中 commit 階段又能夠細分爲 pre-commit 階段和 commit 階段,以下圖所示:
從 v16.3 版本開始,在 render 階段,如下幾個生命週期被認爲是不安全的,它們將在將來的版本中被移除,能夠看到這些生命週期在上圖中未被包括進去,以下所示:
在 React 官網中明確提到了廢棄的緣由,這些被標記爲不安全的生命週期因爲經常被開發者錯誤理解甚至被濫用,好比一些開發人員會傾向於將帶有請求數據等反作用的邏輯放在這些生命週期方法中,認爲能帶來更好的性能,而實際上真正帶來的收益幾乎能夠忽略。在將來, React 逐步推崇異步渲染模式下,這頗有可能會由於不兼容而帶來不少問題。
在 render 階段,React 能夠根據當前可用的時間片處理一個或多個 fiber 節點,而且得益於 fiber 對象中存儲的元素上下文信息以及指針域構成的鏈表結構,使其可以將執行到一半的工做保存在內存的鏈表中。當 React 中止並完成保存的工做後,讓出時間片去處理一些其餘優先級更高的事情。以後,在從新獲取到可用的時間片後,它可以根據以前保存在內存的上下文信息經過快速遍歷的方式找到中止的 fiber 節點並繼續工做。因爲在此階段執行的工做並不會致使任何用戶可見的更改,由於並無被提交到真實的 DOM。因此,咱們說是 fiber 讓調度可以實現暫停、停止以及從新開始等增量渲染的能力。相反,在 commit 階段,work 執行老是同步的,這是由於在此階段執行的工做將致使用戶可見的更改。這就是爲何在 commit 階段, React 須要一次性提交併完成這些工做的緣由。
首次渲染以後,React 會生成一個對應於 UI 渲染的 fiber 樹,稱之爲 current 樹。實際上,React 在調用生命週期鉤子函數時就是經過判斷是否存在 current 來區分什麼時候執行 componentDidMount 和 componentDidUpdate。當 React 遍歷 current 樹時,它會爲每個存在的 fiber 節點建立了一個替代節點,這些節點構成一個 workInProgress 樹。後續全部發生 work 的地方都是在 workInProgress 樹中執行,若是該樹還未建立,則會建立一個 current 樹的副本,做爲 workInProgress 樹。當 workInProgress 樹被提交後將會在 commit 階段的某一子階段被替換成爲 current 樹。
這裏增長兩個樹的主要緣由是爲了不更新的丟失。好比,若是咱們只增長更新到 workInProgress 樹,當 workInProgress 樹經過從 current 樹中克隆而從新開始時,一些更新可能會丟失。一樣的,若是咱們只增長更新到 current 樹,當 workInProgress 樹被提交後會被替換爲 current 樹,更新也會被丟失。經過在兩個隊列都保持更新,能夠確保更新始終是下一個 workInProgress 樹的一部分。而且,由於 workInProgress 樹被提交成爲 current 樹,並不會出現相同的更新而被重複應用兩次的狀況。
effect list 能夠理解爲是一個存儲 effectTag 反作用列表容器。它是由 fiber 節點和指針 nextEffect 構成的單鏈表結構,這其中還包括第一個節點 firstEffect,和最後一個節點 lastEffect。以下圖所示:
React 採用深度優先搜索算法,在 render 階段遍歷 fiber 樹時,把每個有反作用的 fiber 篩選出來,最後構建生成一個只帶反作用的 effect list 鏈表。
在 commit 階段,React 拿到 effect list 數據後,經過遍歷 effect list,並根據每個 effect 節點的 effectTag 類型,從而對相應的 DOM 樹執行更改。
更多 effect list 構建演示流程,能夠點擊查看動畫 《Effect List —— 又一個 Fiber 鏈表的構建過程》。
在本文中,咱們以類組件爲例,假設已經開始調用了一個 setState 方法。
每一個 React 組件都有一個相關聯的 updater,做爲組件層和核心庫之間的橋樑。
react.Component 本質上就是一個函數,在它的原型對象上掛載了 setState 方法
文件位置:react/src/ReactBaseClasses.js
// Component函數 function Component(props, context, updater) { this.props = props; this.context = context; this.updater = updater || ReactNoopUpdateQueue; } // Component原型對象掛載 setState Component.prototype.setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState, callback, 'setState'); };
React 給 work 大體分紅如下幾種優先級類型,其中 immediate 比較特殊,它的優先級最高,能夠理解爲是同步調度,調度過程當中不會被中斷。
export const NoPriority = 0; export const ImmediatePriority = 1; export const UserBlockingPriority = 2; export const NormalPriority = 3; export const LowPriority = 4; export const IdlePriority = 5;
React 有一套計算邏輯,根據不一樣的優先級類型爲不一樣的 work 計算出一個過時時間 expirationTime,其實就是一個時間戳。所謂的 React 在新的更新到來時,能爲不一樣類型的更新分配優先級順序的能力,本質上是根據過時時間 expirationTime 的大小來肯定優先級順序,expirationTime 數值越小,則優先級越高。在相差必定時間範圍內的 work,React 會認爲它們是同一個批次(batch)的,所以這一批次的 work 會在一次更新中完成。
文件位置:react-reconciler/src/ReactFiberClassComponent.js
const classComponentUpdater = { enqueueSetState(inst, payload, callback) { // 獲取 fiber 對象 const fiber = getInstance(inst); const currentTime = requestCurrentTime(); // 計算到期時間 expirationTime const expirationTime = computeExpirationForFiber(currentTime, fiber, suspenseConfig); const update = createUpdate(expirationTime, suspenseConfig); // 插入 update 到隊列 enqueueUpdate(fiber, update); // 調度 work 方法 scheduleWork(fiber, expirationTime); }, };
文件位置:react-reconciler/src/ReactFiberWorkLoop.js
協調過程老是 renderRoot 開始,方法調用棧:scheduleWork --> scheduleCallbackForRoot --> renderRoot
代碼以下:
function renderRoot( root: FiberRoot, expirationTime: ExpirationTime, isSync: boolean, ) | null { do { // 優先級最高,走同步分支 if (isSync) { workLoopSync(); } else { workLoop(); } } while (true); } // 全部的fiber節點都在workLoop 中被處理 function workLoop() { while (workInProgress !== null && !shouldYield()) { workInProgress = performUnitOfWork(workInProgress); } }
全部的 fiber 節點都在 workLoop 方法處理。協調過程老是從最頂層的 hostRoot 節點開始進行 workInProgress 樹的遍歷。可是,React 會跳過已經處理過的 fiber 節點,直到找到還未完成工做的節點。例如,若是在組件樹的深處調用 setState,React 將從頂部開始,但會快速跳過父節點,直到到達調用了 setState 方法的組件。整個過程採用的是深度優先搜索算法,處理完當前 fiber 節點後,workInProgress 將包含對樹中下一個 fiber 節點的引用,若是下一個節點爲 null 不存在,則認爲執行結束退出 workLoop 循環並準備進行一次提交更改。
方法調用棧以下:
performUnitOfWork --> beginWork --> updateClassComponent --> finishedComponent --> completeUnitOfWork
代碼以下所示:
文件位置:react-reconciler/src/ReactFiberWorkLoop.js
function performUnitOfWork(unitOfWork: Fiber): Fiber | null { const current = unitOfWork.alternate; let next; next = beginWork(current, unitOfWork, renderExpirationTime); // 若是沒有新的 work,則認爲已完成當前工做 if (next === null) { next = completeUnitOfWork(unitOfWork); } return next; }
瞭解樹的深度優先搜索算法,可點擊參考該示例 《js-ntqfill》。
文件位置:react-reconciler/src/completeUnitOfWork.js
在 completeUnitOfWork 方法中構建 effect-list 鏈表,該 effect list 在下一個 commit 階段很是重要,關於 effect list 上述有介紹。
以下所示:
function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { // 深度優先搜索算法 workInProgress = unitOfWork; do { const current = workInProgress.alternate; const returnFiber = workInProgress.return; /* 構建 effect-list部分 */ if (returnFiber.firstEffect === null) { returnFiber.firstEffect = workInProgress.firstEffect; } if (workInProgress.lastEffect !== null) { if (returnFiber.lastEffect !== null) { returnFiber.lastEffect.nextEffect = workInProgress.firstEffect; } returnFiber.lastEffect = workInProgress.lastEffect; } if (returnFiber.lastEffect !== null) { returnFiber.lastEffect.nextEffect = workInProgress; } else { returnFiber.firstEffect = workInProgress; } returnFiber.lastEffect = workInProgress; const siblingFiber = workInProgress.sibling; if (siblingFiber !== null) { // If there is more work to do in this returnFiber, do that next. return siblingFiber; } // Otherwise, return to the parent workInProgress = returnFiber; } while (workInProgress !== null); }
至此,一個 render 階段大概流程結束。
commit 階段是 React 更新真實 DOM 並調用 pre-commit phase 和 commit phase 生命週期方法的地方。與 render 階段不一樣,commit 階段的執行始終是同步的,它將依賴上一個 render 階段構建的 effect list 鏈表來完成。
commit 階段實質上被分爲以下三個子階段:
mutation 階段主要作的事情是遍歷 effect-list 列表,拿到每個 effect 存儲的信息,根據反作用類型 effectTag 執行相應的處理並提交更新到真正的 DOM。全部的 mutation effects 都會在 layout phase 階段以前被處理。當該階段執行結束時,workInProgress 樹會被替換成 current 樹。所以在 mutation phase 階段以前的子階段 before mutation,是調用 getSnapshotBeforeUpdate 生命週期的地方。在 before mutation 這個階段,真正的 DOM 尚未被變動。最後一個子階段是 layout phase,在這個階段生命週期 componentDidMount/Update 被執行。
文件位置:react-reconciler/src/ReactFiberWorkLoop.js
以下所示:
function commitRootImpl(root) { if (firstEffect !== null) { // before mutation 階段,遍歷 effect list do { try { commitBeforeMutationEffects(); } catch (error) { nextEffect = nextEffect.nextEffect; } } while (nextEffect !== null); // the mutation phase 階段,遍歷 effect list nextEffect = firstEffect; do { try { commitMutationEffects(); } catch (error) { nextEffect = nextEffect.nextEffect; } } while (nextEffect !== null); // 將 work-in-progress 樹替換爲 current 樹 root.current = finishedWork; // layout phase 階段,遍歷 effect list nextEffect = firstEffect; do { try { commitLayoutEffects(root, expirationTime); } catch (error) { captureCommitPhaseError(nextEffect, error); nextEffect = nextEffect.nextEffect; } } while (nextEffect !== null); nextEffect = null; } else { // No effects. root.current = finishedWork; } }
before mutation 調用鏈路:commitRootImpl --> commitBeforeMutationEffects --> commitBeforeMutationLifeCycles
代碼以下:
function commitBeforeMutationLifeCycles( current: Fiber | null, finishedWork: Fiber, ): void { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: ... // 屬性 stateNode 表示對應組件的實例 // 在這裏 class 組件實例執行 instance.getSnapshotBeforeUpdate() case ClassComponent: { if (finishedWork.effectTag & Snapshot) { if (current !== null) { const prevProps = current.memoizedProps; const prevState = current.memoizedState; const instance = finishedWork.stateNode; const snapshot = instance.getSnapshotBeforeUpdate( finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState, ); instance.__reactInternalSnapshotBeforeUpdate = snapshot; } } return; } case HostRoot: case HostComponent: case HostText: case HostPortal: case IncompleteClassComponent: ... } }
文件位置:react-reconciler/src/ReactFiberWorkLoop.js
mutation phase 階段調用鏈路:
commitRootImpl --> commitMutationEffects --> commitWork
代碼以下:
function commitMutationEffects() { while (nextEffect !== null) { const effectTag = nextEffect.effectTag; let primaryEffectTag = effectTag & (Placement | Update | Deletion); switch (primaryEffectTag) { case Placement: ... case PlacementAndUpdate: ... case Update: { const current = nextEffect.alternate; commitWork(current, nextEffect); break; } case Deletion: { commitDeletion(nextEffect); break; } } } }
文件位置:react-reconciler/src/ReactFiberCommitWork.js
layout phase 調用鏈路:commitRootImpl --> commitLayoutEffects --> commitLifeCycles
代碼以下:
function commitLifeCycles( finishedRoot: FiberRoot, current: Fiber | null, finishedWork: Fiber, committedExpirationTime: ExpirationTime, ): void { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: ... case ClassComponent: { // 屬性 stateNode 表示對應組件的實例 // 在這裏 class 組件實例執行 componentDidMount/DidUpdate const instance = finishedWork.stateNode; if (finishedWork.effectTag & Update) { // 首次渲染時,尚未 current 樹 if (current === null) { instance.componentDidMount(); } else { const prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps : resolveDefaultProps(finishedWork.type, current.memoizedProps); const prevState = current.memoizedState; instance.componentDidUpdate( prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate, ); } } const updateQueue = finishedWork.updateQueue; if (updateQueue !== null) { commitUpdateQueue( finishedWork, updateQueue, instance, committedExpirationTime, ); } return; } case HostRoot: case HostComponent: case HostText: case HostPortal: case Profiler: case SuspenseComponent: case SuspenseListComponent: ... } }
如下是一些關於 Fiber 的擴展內容。
以下圖所示,根據 React 源碼繪製的調用鏈路圖,主要羅列了一些比較重要的函數方法,可做爲你們瞭解 Fiber 的參考。源碼調試過程能夠找到對應的函數方法打斷點,以瞭解實際運行的過程,便於更好梳理出各個邏輯方法之間的關係。
以前有文章在總結 React Fiber 的調度原理時提到,客戶端線程執行任務時會以幀的形式劃分,在兩個執行幀之間,主線程一般會有一小段空閒時間,在這個空閒期觸發 requestIdleCallback 方法,可以執行一些優先級較低的 work。
聽說在早期的 React 版本上確實是這麼作的,但使用 requestIdleCallback 實際上有一些限制,執行頻次不足,以至於沒法實現流暢的 UI 渲染,擴展性差。所以 React 團隊放棄了 requestIdleCallback 用法,實現了自定義的版本。好比,在發佈 v16.10 版本中,推出實驗性的 Scheduler,嘗試使用 postMessage 來代替 requestAnimationFrame。更多瞭解能夠查看 React 源碼 packages/scheduler 部分。
Fiber 由來已久,能夠說是 React 設計思想的一個典型表現。相比業界其餘流行庫更多采用當新數據到達時再計算模式,React 堅持拉取模式,即可以把計算資源延遲到必要時候再用,而且它知道,何時更適合執行,何時不執行。看起來雖然只是微小的區別,卻意義很大。隨着後續異步渲染能力等新特性的推出,咱們有理由相信,在將來,React 將會在人機交互的應用中給咱們帶來更多的驚喜。
本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!