react16-Fiber架構:改變了之前react的組件渲染機制,新的架構使原來同步渲染的組件現在可以異步化,可中途中斷渲染,執行更高優先級的任務,釋放瀏覽器主線程。
React 核心算法的更新 —— 這次更新提供了一個從底層重寫了 React 的 reconciliation 算法(譯注:reconciliation 算法,是 React 用來比較兩棵 DOM 樹差異、從而判斷哪一部分應當被更新的算法)。這次算法重寫帶來的主要特性是異步渲染。異步渲染的意義在於能夠將渲染任務划分為多塊。瀏覽器的渲染引擎是單線程的,這意味着幾乎所有的行為都是同步發生的。React 16 使用原生的瀏覽器 API 來間歇性地檢查當前是否還有其他任務需要完成,從而實現了對主線程和渲染過程的管理。在之前的版本中,React 會在計算 DOM 樹的時候鎖住整個線程。這個 reconciliation 的過程現在被稱作 “stack reconciliation”。盡管 React 已經是以快而聞名了,但是鎖住整個線程也會讓一些應用運行得不是很流暢。16 這個版本通過不要求渲染過程在初始化后一次性完成修復了該問題。React 計算了 DOM 樹的一部分,之后將暫停渲染,來看看主線程是否有任何的繪圖或者更新需要去完成。一旦繪圖和更新完成了,React 就會繼續渲染。這個過程通過引入了一個新的,叫做 “fiber” 的數據結構完成,fiber 映射到了一個 React 實例並為該實例管理其渲染任務,它也知道它和其他 fiber 之間的關系。
react16以前的組件渲染方式存在一個問題,如果這是一個很大,層級很深的組件,react渲染它需要幾十甚至幾百毫秒,在這期間,react會一直占用瀏覽器主線程,任何其他的操作(包括用戶的點擊,鼠標移動等操作)都無法執行。好似一個潛水員,當它一頭扎進水里,就要往最底層一直游,直到找到最底層的組件,然后他再上岸。在這期間,岸上發生的任何事,都不能對他進行干擾,如果有更重要的事情需要他去做(如用戶操作),也必須得等他上岸。fiber架構—組件的渲染順序:潛水員會每隔一段時間就上岸,看是否有更重要的事情要做。加入fiber的react將組件更新分為兩個時期(phase 1 && phase 2),render前的生命周期為phase1,render后的生命周期為phase2。
phase1的生命周期是可以被打斷的,每隔一段時間它會跳出當前渲染進程,去確定是否有其他更重要的任務。此過程,React 在 workingProgressTree (並不是真實的virtualDomTree)上復用 current 上的 Fiber 數據結構來一步步地構建新的 tree,標記需要更新的節點,放入隊列中。phase2的生命周期是不可被打斷的,React 將其所有的變更一次性更新到DOM上。這里最重要的是phase1這是時期所做的事。因此我們需要具體了解phase1的機制。
如果不被打斷,那么phase1執行完會直接進入render函數,構建真實的virtualDomTree。如果組件再phase1過程中被打斷,即當前組件只渲染到一半(也許是在willMount,也許是willUpdate~反正是在render之前的生命周期),那么react會怎么干呢? react會放棄當前組件所有干到一半的事情,去做更高優先級更重要的任務(當然,也可能是用戶鼠標移動,或者其他react監聽之外的任務)。當所有高優先級任務執行完之后,react通過callback回到之前渲染到一半的組件,從頭開始渲染。
React 16 也會在必要的時候管理各個更新的優先級。這就允許了高優先級更新能夠排到隊列開頭從而被首先處理。關於此的一個例子就是按鍵輸入。鑒於應用流暢性的考慮,用戶需要立即獲得按鍵響應,因而相對於那些可以等待 100-200 毫秒的低優先級更新任務,按鍵輸入擁有較高優先級。
- Fiber節點的數據結構
{ tag: TypeOfWork, // fiber的類型,下一節會介紹 alternate: Fiber|null, // 在fiber更新時克隆出的鏡像fiber,對fiber的修改會標記在這個fiber上 return: Fiber|null, // 指向fiber樹中的父節點 child: Fiber|null, // 指向第一個子節點 sibling: Fiber|null, // 指向兄弟節點 effectTag: TypeOfSideEffect, // side effect類型,下文會介紹 nextEffect: Fiber | null, // 單鏈表結構,方便遍歷fiber樹上有副作用的節點 pendingWorkPriority: PriorityLevel, // 標記子樹上待更新任務的優先級 }
在實際的渲染過程中,Fiber節點構成了一顆樹。這棵樹在數據結構上是通過單鏈表的形式構成的,Fiber節點上的chlid
和sibling
屬性分別指向了這個節點的第一個子節點和相鄰的兄弟節點。這樣就可以遍歷整個Fiber樹了。
- TypeOfWork:代表React中不同類型的fiber節點。
{ IndeterminateComponent: 0, // Before we know whether it is functional or class FunctionalComponent: 1, ClassComponent: 2, HostRoot: 3, // Root of a host tree. Could be nested inside another node. HostPortal: 4, // A subtree. Could be an entry point to a different renderer. HostComponent: 5, HostText: 6, CoroutineComponent: 7, CoroutineHandlerPhase: 8, YieldComponent: 9, Fragment: 10, }
ClassComponent:就是應用層面的React組件。ClassComponent是一個繼承自React.Component的類的實例。
HostRoot:ReactDOM.render()時的根節點。
HostComponent:React中最常見的抽象節點,是ClassComponent的組成部分。具體的實現取決於React運行的平台。在瀏覽器環境下就代表DOM節點,可以理解為所謂的虛擬DOM節點。HostComponent中的Host就代碼這種組件的具體操作邏輯是由Host環境注入的。
- TypeOfSideEffect:這是以二進制位表示的,可以多個疊加。
{ NoEffect: 0, PerformedWork: 1, Placement: 2, // 插入 Update: 4, // 更新 PlacementAndUpdate: 6, Deletion: 8, // 刪除 ContentReset: 16, Callback: 32, Err: 64, Ref: 128, }
- setState:用戶觸發的
setState
開啟的一次渲染
Component.prototype.setState = function (partialState, callback) { !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? invariant_1(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : void 0; this.updater.enqueueSetState(this, partialState, callback, 'setState'); };
setState
調用了this.updater.enqueueSetState
。updater是renderer在渲染的時候注入的對象,這個對象由reconciler提供。
var classComponentUpdater = { isMounted: isMounted, enqueueSetState: function (inst, payload, callback) { var fiber = get(inst); // 從全局拿到React組件實例對應的fiber var currentTime = recalculateCurrentTime(); var expirationTime = computeExpirationForFiber(currentTime, fiber); // 計算fiber的優先級 var update = createUpdate(expirationTime); update.payload = payload; if (callback !== undefined && callback !== null) { { warnOnInvalidCallback$1(callback, 'setState'); } update.callback = callback; } enqueueUpdate(fiber, update, expirationTime); // 向隊列中推入需要更新的fiber scheduleWork$1(fiber, expirationTime); // 觸發調度器調度一次新的更新 }, //... }
- performUnitOfWork:React 16保持了之前版本的事務風格,一個“work”會被分解為begin和complete兩個階段來完成。
function performUnitOfWork(workInProgress) { // The current, flushed, state of this fiber is the alternate. // Ideally nothing should rely on this, but relying on it here // means that we don't need an additional field on the work in // progress. var current = workInProgress.alternate; // See if beginning this work spawns more work. startWorkTimer(workInProgress); { ReactDebugCurrentFiber.setCurrentFiber(workInProgress); } if (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) { stashedWorkInProgressProperties = assignFiberPropertiesInDEV(stashedWorkInProgressProperties, workInProgress); } var next = void 0; if (enableProfilerTimer) { if (workInProgress.mode & ProfileMode) { startBaseRenderTimer(); } next = beginWork(current, workInProgress, nextRenderExpirationTime); if (workInProgress.mode & ProfileMode) { // Update "base" time if the render wasn't bailed out on. recordElapsedBaseRenderTimeIfRunning(workInProgress); stopBaseRenderTimerIfRunning(); } } else { next = beginWork(current, workInProgress, nextRenderExpirationTime); } // ... if (next === null) { // If this doesn't spawn new work, complete the current work. next = completeUnitOfWork(workInProgress); } ReactCurrentOwner.current = null; return next; }
- beginWork:根據fiber節點不同的tag,調用對應的update方法。可以說是一個入口函數。
function beginWork(current, workInProgress, renderExpirationTime) { // ... switch (workInProgress.tag) { case ClassComponent: return updateClassComponent(current, workInProgress, renderExpirationTime); // ClassComponent對應的是React組件實例 case HostRoot: return updateHostRoot(current, workInProgress, renderExpirationTime); case HostComponent: return updateHostComponent(current, workInProgress, renderExpirationTime); // HostComponent對應的是一個視圖層節點,在瀏覽器環境中就等於DOM節點 // ... } }
- updateClassComponent
updateHostComponent
:HostComponent沒有生命周期鈎子需要處理,這個函數主要做的就是調用reconcileChildren
對子節點進行diff。
- reconcileChildren:Virtul DOM diff
// TODO: Remove this and use reconcileChildrenAtExpirationTime directly. function reconcileChildren(current, workInProgress, nextChildren) { reconcileChildrenAtExpirationTime(current, workInProgress, nextChildren, workInProgress.expirationTime); } function reconcileChildrenAtExpirationTime(current, workInProgress, nextChildren, renderExpirationTime) { if (current === null) { // 首次渲染,創建子節點fiber實例 // If this is a fresh new component that hasn't been rendered yet, we // won't update its child set by applying minimal side-effects. Instead, // we will add them all to the child before it gets rendered. That means // we can optimize this reconciliation pass by not tracking side-effects. workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderExpirationTime); } else { // 未處理過子節點;處理過子節點被中斷,丟棄之前的處理工作 // If the current child is the same as the work in progress, it means that // we haven't yet started any work on these children. Therefore, we use // the clone algorithm to create a copy of all the current children. // If we had any progressed work already, that is invalid at this point so // let's throw it out. workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderExpirationTime); } }
var reconcileChildFibers = ChildReconciler(true); var mountChildFibers = ChildReconciler(false);
mountChildFibers
、reconcileChildFibers
這兩個函數其實是同一個函數,通過傳入不同的參數“重載”而來的。
ChildReconciler是一個工廠函數,它接收shouldTrackSideEffects這個參數。reconcileChildFibers函數的目的是產出effect list,mountChildFibers是組件初始化時用的,所以不用clone fiber來diff,也不用產出effect list。ChildReconciler內部有很多helper函數,最終返回的函數叫reconcileChildFibers,這個函數實現了對子fiber節點的reconciliation。
總的,這個函數根據newChild的類型調用不同的方法。newChild可能是一個元素,也可能是一個數組(React16新特性)。如果是reconcile單個元素,以reconcileSingleElement為例比較key和type,如果相同,復用fiber,刪除多余的元素(currentFirstChild的sibling),如果不同,調用createFiberFromElement,返回新創建的。
如果是string,reconcileSingleTextNode;如果是array,reconcileChildrenArray;如果是空,deleteRemainingChildren刪除老的子元素
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, expirationTime) { // This function is not recursive. // If the top level item is an array, we treat it as a set of children, // not as a fragment. Nested arrays on the other hand will be treated as // fragment nodes. Recursion happens at the normal flow. // Handle top level unkeyed fragments as if they were arrays. // This leads to an ambiguity between <>{[...]}</> and <>...</>. // We treat the ambiguous cases above the same. var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null; if (isUnkeyedTopLevelFragment) { newChild = newChild.props.children; } // Handle object types var isObject = typeof newChild === 'object' && newChild !== null; if (isObject) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, expirationTime)); case REACT_PORTAL_TYPE: return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, expirationTime)); } } if (typeof newChild === 'string' || typeof newChild === 'number') { return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, expirationTime)); } if (isArray$1(newChild)) { return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, expirationTime); } if (getIteratorFn(newChild)) { return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, expirationTime); } if (isObject) { throwOnInvalidObjectType(returnFiber, newChild); } { if (typeof newChild === 'function') { warnOnFunctionType(); } } // ... // Remaining cases are all treated as empty. return deleteRemainingChildren(returnFiber, currentFirstChild); }
React的reconcile算法采用的是層次遍歷,這種算法是建立在一個節點的插入、刪除、移動等操作都是在節點樹的同一層級中進行這個假設下的。所以reconcile算法的核心就是如何diff兩個子節點數組。
- reconcileChildrenArray:React16的diff算法采用和來自社區的兩端同時比較法同樣結構的算法
因為fiber樹是單鏈表結構,沒有子節點數組這樣的數據結構,也就沒有可以供兩端同時比較的尾部游標。所以React的這個算法是一個簡化的兩端比較法,只從頭部開始比較。第一次遍歷新數組,對上了,新老index都++,比較新老數組哪些元素是一樣的,(通過updateSlot,比較key),如果是同樣的就update。第一次遍歷玩了,如果新數組遍歷完了,那就可以把老數組中剩余的fiber刪除了。如果老數組完了新數組還沒完,那就把新數組剩下的都插入。如果這些情況都不是,就把所有老數組元素按key放map里,然后遍歷新數組,插入老數組的元素,這是移動的情況。最后再刪除沒有被上述情況涉及的元素(也就是老數組中有新數組中無的元素,上面的刪除只是fast path,特殊情況)
- completeUnitOfWork
completeUnitOfWork
是complete階段的入口。complete階段的作用就是在一個節點diff完成之后,對它進行一些收尾工作,主要是更新props和調用生命周期方法等等。completeUnitOfWork
主要的邏輯是調用completeWork
完成收尾,然后將當前子樹的effect list插入到HostRoot的effect list中。
- completeWork:complete階段主要工作都是在
completeWork
中完成的
completeWork主要是完成reconciliation階段的掃尾工作,重點是對HostComponent的props進行diff,並標記更新。reconciliation階段主要負責產出effect list。reconcile的過程相當於是一個純函數,輸入是fiber節點,輸出一個effect list。side-effects是在commit階段被應用到UI中的,這樣就將side-effects從reconciliation中隔離開了。因為純函數的可預測性,讓我們可以隨時中斷reconciliation階段的執行,而不用擔心side-effects給讓組件狀態和實際UI產生不一致。
- commitRoot
reconciliation階段結束之后,我們需要將effect list更新到UI中。這就是commit節點的工作。commit這個階段有點像Git的commit概念。在緩沖區中的代碼改動只有在commit之后才會被添加到Git的Object store中。