react16源碼(Fiber架構)


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節點上的chlidsibling屬性分別指向了這個節點的第一個子節點和相鄰的兄弟節點。這樣就可以遍歷整個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);

mountChildFibersreconcileChildFibers這兩個函數其實是同一個函數,通過傳入不同的參數“重載”而來的。

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中。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM