走進React Fiber 架構

本文重點:介紹React重構的原由和目的,理解Fiber tree單向鏈表結構中各屬性含義,梳理調度過程和核心實現手段,深刻新的生命週期,hooks,suspense,異常捕獲等特性的用法和原理。css

喜歡的就點個贊吧️,但願跟你們在枯燥的源碼中發掘學習的樂趣,一塊兒分享進步。html

當react剛推出的時候,最具革命性的特性就是虛擬dom,由於這大大下降了應用開發的難度,相比較以往告訴瀏覽器我須要怎麼更新個人ui,如今咱們只須要告訴react我應用ui的下個狀態是怎麼樣的,react會幫咱們自動處理二者之間的全部事宜。前端

這讓咱們能夠從屬性操做、事件處理和手動 DOM 更新這些在構建應用程序時必要的操做中解放出來。宿主樹的概念讓這個優秀的框架有無限的可能性,react native即是其在原生移動應用中偉大的實現。react

但在享受溫馨開發體驗的同時,有一些疑問一直縈繞在咱們腦海中:git

  • 是什麼致使了react用戶交互、動畫頻繁卡頓
  • 如何視線優雅的異常處理,進行異常捕獲和備用ui渲染
  • 如何更好實現組件的複用和狀態管理

這到底是人性的扭曲,仍是道德的淪喪 /狗頭github

Fiber可否給咱們答案,又將帶給咱們什麼驚喜,捲起一波新的浪潮,歡迎收看《走進Fiber》ajax

那麼,簡而言之,React Fiber是什麼?算法

Fiber是對React核心算法的重構,2年重構的產物就是Fiber reconciler。segmentfault

react協調是什麼

協調是react中重要的一部分,其中包含了如何對新舊樹差別進行比較以達到僅更新差別的部分。promise

如今的react通過重構後Reconciliation和Rendering被分爲兩個不一樣的階段。

  • reconciler協調階段:當組件次初始化和其後的狀態更新中,React會建立兩顆不相同的虛擬樹,React 須要基於這兩棵樹之間的差異來判斷如何有效率的更新 UI 以保證當前 UI 與最新的樹保持同步,計算樹哪些部分須要更新。
  • renderer階段:渲染器負責將拿到的虛擬組件樹信息,根據其對應環境真實地更新渲染到應用中。有興趣的朋友能夠看一下dan本身的博客中的文章=》運行時的react=》渲染器,介紹了react的Renderer渲染器如react-dom和react native等,其能夠根據不一樣的主環境來生成不一樣的實例。

爲何要重寫協調

動畫是指由許多幀靜止的畫面,以必定的速度(如每秒16張)連續播放時,肉眼因視覺殘象產生錯覺,而誤覺得畫面活動的做品。——維基百科

老一輩人經常把電影稱爲「移動的畫」,咱們小時候看的手翻書就是快速翻動的一頁頁畫,其本質上實現原理跟動畫是同樣的。

幀:在動畫過程當中,每一幅靜止畫面即爲一「幀」;
幀率:是用於測量顯示幀數的量度,測量單位爲「每秒顯示幀數」(Frame per Second,FPS)或「赫茲」;
幀時長:即每一幅靜止畫面的停留時間,單位通常是ms(毫秒);
丟幀:在幀率固定的動畫中,某一幀的時長遠高於平均幀時長,致使其後續數幀被擠壓而丟失的現象;

當前大部分筆記本電腦和手機的常見幀率爲60hz,即一秒顯示60幀的畫面,一幀停留的時間爲16.7ms(1000/60≈16.7),這就留給了開發者和UI系統大約16.67ms來完成生成一張靜態圖片(幀)所須要的全部工做。若是在這分派的16.67ms以內沒有可以完成這些工做,就會引起‘丟幀’的後果,使界面表現的不夠流暢。

瀏覽器中的GUI渲染線程和JS引擎線程

在瀏覽器中GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(至關於被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閒時當即被執行。

瀏覽器擁擠的主線程

React16 推出Fiber以前協調算法是Stack Reconciler,即遞歸遍歷全部的 Virtual DOM 節點執行Diff算法,一旦開始便沒法中斷,直到整顆虛擬dom樹構建完成後纔會釋放主線程,因其JavaScript單線程的特色,若當下組件具備複雜的嵌套和邏輯處理,diff便會堵塞UI進程,使動畫和交互等優先級相對較高的任務沒法當即獲得處理,形成頁面卡頓掉幀,影響用戶體驗。

16年在 facebook 上 Seb 正式提到了 Fiber 這個概念,解釋爲何要重寫框架:

Once you have each stack frame as an object on the heap you can do clever things like reusing it during future updates and yielding to the event loop without losing any of your currently in progress data.
一旦將每一個堆棧幀做爲堆上的對象,您就能夠作一些聰明的事情,例如在未來的更新中重用它並暫停於事件循環,而不會丟失任何當前正在進行的數據。

咱們來作一個實驗

function randomHexColor() {
  return (
    "#" + ("0000" + ((Math.random() * 0x1000000) << 0).toString(16)).substr(-6)
  );
}

var root = document.getElementById("root");

// 一次性遍歷100000次
function a() {
  setTimeout(function() {
    var k = 0;
    for (var i = 0; i < 10000; i++) {
      k += new Date() - 0;
      var el = document.createElement("div");
      el.innerHTML = k;
      root.appendChild(el);
      el.style.cssText = `background:${randomHexColor()};height:40px`;
    }
  }, 1000);
}

// 每次只操做100個節點,共100次
function b() {
  setTimeout(function() {
    function loop(n) {
      var k = 0;
      console.log(n);
      for (var i = 0; i < 100; i++) {
        k += new Date() - 0;
        var el = document.createElement("div");
        el.innerHTML = k;
        root.appendChild(el);
        el.style.cssText = `background:${randomHexColor()};height:40px`;
      }
      if (n) {
        setTimeout(function() {
          loop(n - 1);
        }, 40);
      }
    }
    loop(100);
  }, 1000);
}
複製代碼

a執行性能截圖:掉幀嚴重,廣泛fps爲1139.6ms

b執行性能截圖: fps處於15ms~19ms

究其緣由是由於瀏覽器的主線程須要處理GUI描繪,時間器處理,事件處理,JS執行,遠程資源加載等,當作某件事,只有將它作完才能作下一件事。若是有足夠的時間,瀏覽器是會對咱們的代碼進行編譯優化(JIT)及進行熱代碼優化,一些DOM操做,內部也會對reflow進行處理。reflow是一個性能黑洞,極可能讓頁面的大多數元素進行從新佈局。

而做爲一隻有夢想的前端菜🐤,爲用戶爸爸呈現最好的交互體驗是咱們責無旁貸的責任,把困難扛在肩上,讓咱們see see react是如何解決以上的問題。

Fiber你是個啥(第四音

那麼咱們先看看做爲看看解決方案的Fiber是什麼,而後在分析爲何它能解決以上問題。

定義:

  1. react Reconciliation協調核心算法的一次從新實現
  2. 虛擬堆棧幀
  3. 具有扁平化的鏈表數據存儲結構的js對象,Reconciliation階段所能拆分的最小工做單元

針對其定義咱們來進行拓展:

虛擬堆棧幀:

Andrew Clark的React Fiber體系文檔很好地解釋了Fiber實現背後的想法,我在這裏引用一下:

Fiber是堆棧的從新實現,專門用於React組件。 您能夠將單個Fiber視爲虛擬堆棧框架。 從新實現堆棧的優勢是,您能夠將堆棧幀保留在內存中,並根據須要(以及在任什麼時候候)執行它們。 這對於實現調度的目標相當重要。

JavaScript的執行模型:call stack

JavaScript原生的執行模型:經過調用棧來管理函數執行狀態。
其中每一個棧幀表示一個工做單元(a unit of work),存儲了函數調用的返回指針、當前函數、調用參數、局部變量等信息。 由於JavaScript的執行棧是由引擎管理的,執行棧一旦開始,就會一直執行,直到執行棧清空。沒法按需停止。

react以往的渲染就是使用原生執行棧來管理組件樹的遞歸渲染,當其層次較深component不斷遞歸子節點,沒法被打斷就會致使主線程堵塞ui卡頓。

可控的調用棧

因此理想情況下reconciliation的過程應該是像下圖所示同樣,將繁重的任務劃分紅一個個小的工做單元,作完後可以「喘口氣兒」。咱們須要一種增量渲染的調度,Fiber就是從新實現一個堆棧幀的調度,這個堆棧幀能夠按照本身的調度算法執行他們。另外因爲這些堆棧是可將可中斷的任務拆分紅多個子任務,經過按照優先級來自由調度子任務,分段更新,從而將以前的同步渲染改成異步渲染。

它的特性就是時間分片(time slicing)和暫停(supense)。

具有扁平化的鏈表數據存儲結構的js對象:

fiber是一個js對象,fiber的建立是經過React元素來建立的,在整個React構建的虛擬DOM樹中,每個元素都對應有一個fiber,從而構建了一棵fiber樹,每一個fiber不只僅包含每一個元素的信息,還包含更多的信息,以方便Scheduler來進行調度。

讓咱們看一下fiber的結構

type Fiber = {|
  // 標記不一樣的組件類型
  //export const FunctionComponent = 0;
  //export const ClassComponent = 1;
  //export const HostRoot = 3; 能夠理解爲這個fiber是fiber樹的根節點,根節點能夠嵌套在子樹中
  //export const Fragment = 7;
  //export const SuspenseComponent = 13;
  //export const MemoComponent = 14;
  //export const LazyComponent = 16;
  tag: WorkTag,

  // ReactElement裏面的key
  // 惟一標示。咱們在寫React的時候若是出現列表式的時候,須要制定key,這key就是對應元素的key。
  key: null | string,

  // ReactElement.type,也就是咱們調用`createElement`的第一個參數
  elementType: any,

  // The resolved function/class/ associated with this fiber.
  // 異步組件resolved以後返回的內容,通常是`function`或者`class`
  type: any,

  // The local state associated with this fiber.
  // 跟當前Fiber相關本地狀態(好比瀏覽器環境就是DOM節點)
  // 當前組件實例的引用
  stateNode: any,

  // 指向他在Fiber節點樹中的`parent`,用來在處理完這個節點以後向上返回
  return: Fiber | null,

  // 單鏈表樹結構
  // 指向本身的第一個子節點
  child: Fiber | null,
  // 指向本身的兄弟結構
  // 兄弟節點的return指向同一個父節點
  sibling: Fiber | null,
  index: number,

  // ref屬性
  ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,

  // 新的變更帶來的新的props
  pendingProps: any, 
  // 上一次渲染完成以後的props
  memoizedProps: any,

  // 該Fiber對應的組件產生的Update會存放在這個隊列裏面
  updateQueue: UpdateQueue<any> | null,

  // 上一次渲染的時候的state
  // 用來存放某個組件內全部的 Hook 狀態
  memoizedState: any,

  // 一個列表,存放這個Fiber依賴的context
  firstContextDependency: ContextDependency<mixed> | null,

  // 用來描述當前Fiber和他子樹的`Bitfield`
  // 共存的模式表示這個子樹是否默認是異步渲染的
  // Fiber被建立的時候他會繼承父Fiber
  // 其餘的標識也能夠在建立的時候被設置
  // 可是在建立以後不該該再被修改,特別是他的子Fiber建立以前
  //用來描述fiber是處於何種模式。用二進制位來表示(bitfield),後面經過與來看二者是否相同//這個字段實際上是一個數字.實現定義了一下四種//NoContext: 0b000->0//AsyncMode: 0b001->1//StrictMode: 0b010->2//ProfileMode: 0b100->4
  mode: TypeOfMode,

  // Effect
  // 用來記錄Side Effect具體的執行的工做的類型:好比Placement,Update等等
  effectTag: SideEffectTag,

  // 單鏈表用來快速查找下一個side effect
  nextEffect: Fiber | null,

  // 子樹中第一個side effect
  firstEffect: Fiber | null,
  // 子樹中最後一個side effect
  lastEffect: Fiber | null,

  // 表明任務在將來的哪一個時間點應該被完成
  // 不包括他的子樹產生的任務
  // 經過這個參數也能夠知道是否還有等待暫停的變動、沒有完成變動。
  // 這個參數通常是UpdateQueue中最長過時時間的Update相同,若是有Update的話。
  expirationTime: ExpirationTime,

  // 快速肯定子樹中是否有不在等待的變化
  childExpirationTime: ExpirationTime,

  //當前fiber對應的工做中的Fiber。
  // 在Fiber樹更新的過程當中,每一個Fiber都會有一個跟其對應的Fiber
  // 咱們稱他爲 current <==> workInProgress
  // 在渲染完成以後他們會交換位置
  alternate: Fiber | null,
  ...
|};
複製代碼

ReactWorkTags組件類型

鏈表結構

fiber中最爲重要的是return、child、sibling指針,鏈接父子兄弟節點以構成一顆單鏈表fiber樹,其扁平化的單鏈表結構的特色將以往遞歸遍歷改成了循環遍歷,實現深度優先遍歷。

React16特別青睞於鏈表結構,鏈表在內存裏不是連續的,動態分配,增刪方便,輕量化,對異步友好

current與workInProgress

current樹:React 在 render 第一次渲染時,會經過 React.createElement 建立一顆 Element 樹,能夠稱之爲 Virtual DOM Tree,因爲要記錄上下文信息,加入了 Fiber,每個 Element 會對應一個 Fiber Node,將 Fiber Node 連接起來的結構成爲 Fiber Tree。它反映了用於渲染 UI 和映射應用狀態。這棵樹一般被稱爲 current 樹(當前樹,記錄當前頁面的狀態)。

workInProgress樹:當React通過current當前樹時,對於每個先存在的fiber節點,它都會建立一個替代(alternate)節點,這些節點組成了workInProgress樹。這個節點是使用render方法返回的React元素的數據建立的。一旦更新處理完以及全部相關工做完成,React就有一顆替代樹來準備刷新屏幕。一旦這顆workInProgress樹渲染(render)在屏幕上,它便成了當前樹。下次進來會把current狀態複製到WIP上,進行交互複用,而不用每次更新的時候都建立一個新的對象,消耗性能。這種同時緩存兩棵樹進行引用替換的技術被稱爲雙緩衝技術

function createWorkInProgress(current, ...) {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    workInProgress = createFiber(...);
  }
  ...
  workInProgress.alternate = current;
  current.alternate = workInProgress;
  ...
  return workInProgress;
}
複製代碼

alternate fiber能夠理解爲一個fiber版本池,用於交替記錄組件更新(切分任務後變成多階段更新)過程當中fiber的更新,由於在組件更新的各階段,更新前及更新過程當中fiber狀態並不一致,在須要恢復時(如發生衝突),便可使用另外一者直接回退至上一版本fiber。

Dan在Beyond React 16演講中用了一個很是恰當的比喻,那就是Git 功能分支,你能夠將 WIP 樹想象成從舊樹中 Fork 出來的功能分支,你在這新分支中添加或移除特性,即便是操做失誤也不會影響舊的分支。當你這個分支通過了測試和完善,就能夠合併到舊分支,將其替換掉。

Update

  • 用於記錄組件狀態的改變
  • 存放於fiber的updateQueue裏面
  • 多個update同時存在

好比設置三個setState(),React是不會當即更新的,而是放到UpdateQueue中,再去更新

ps: setState一直有人疑問爲啥不是同步,將 setState() 視爲請求而不是當即更新組件的命令。爲了更好的感知性能,React 會延遲調用它,而後經過一次傳遞更新多個組件。React 並不會保證 state 的變動會當即生效。

export function createUpdate(
  expirationTime: ExpirationTime,
  suspenseConfig: null | SuspenseConfig,
): Update<*> {
  let update: Update<*> = {
    //任務過時事件
    //在建立每一個更新的時候,須要設定過時時間,過時時間也就是優先級。過時時間越長,就表示優先級越低。
    expirationTime,
    // suspense的配置
    suspenseConfig,

  // export const UpdateState = 0; 表示更新State
  // export const ReplaceState = 1; 表示替換State
  // export const ForceUpdate = 2; 強制更新
  // export const CaptureUpdate = 3; 捕獲更新(發生異常錯誤的時候發生)
  // 指定更新的類型,值爲以上幾種
    tag: UpdateState,
    // 更新內容,好比`setState`接收的第一個參數
    payload: null,
    // 更新完成後的回調,`setState`,`render`都有
    callback: null,

    // 指向下一個update
    // 單鏈表update queue經過 next串聯
    next: null,
    
    // 下一個side effect
    // 最新源碼被拋棄 next替換
    //nextEffect: null,
  };
  if (__DEV__) {
    update.priority = getCurrentPriorityLevel();
  }
  return update;
}
複製代碼

UpdateQueue

//建立更新隊列
export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
  const queue: UpdateQueue<State> = {
    //應用更新後的state
    baseState,
    //隊列中的第一個update
    firstUpdate: null,
    //隊列中的最後一個update
    lastUpdate: null,
     //隊列中第一個捕獲類型的update
    firstCapturedUpdate: null,
    //隊列中最後一個捕獲類型的update
    lastCapturedUpdate: null,
    //第一個side effect
    firstEffect: null,
    //最後一個side effect
    lastEffect: null,
    firstCapturedEffect: null,
    lastCapturedEffect: null,
  };
  return queue;
}
複製代碼

update中的payload:一般咱們如今在調用setState傳入的是一個對象,但在使用fiber conciler時,必須傳入一個函數,函數的返回值是要更新的state。react從很早的版本就開始支持這種寫法了,不過一般沒有人用。在以後的react版本中,可能會廢棄直接傳入對象的寫法。

setState({}, callback); // stack conciler
setState(() => { return {} }, callback); // fiber conciler
複製代碼

ReactUpdateQueue源碼

Updater

每一個組件都會有一個Updater對象,它的用處就是把組件元素更新和對應的fiber關聯起來。監聽組件元素的更新,並把對應的更新放入該元素對應的fiber的UpdateQueue裏面,而且調用ScheduleWork方法,把最新的fiber讓scheduler去調度工做。

const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = getInstance(inst);
    const currentTime = requestCurrentTimeForUpdate();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );

    const update = createUpdate(expirationTime, suspenseConfig);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueReplaceState(inst, payload, callback) {
    //同樣的代碼
    //...
    update.tag = ReplaceState;
    //...
  },
  enqueueForceUpdate(inst, callback) {
    //同樣的代碼
    //...
    update.tag = ForceUpdate;
    //...
  },
};
複製代碼

ReactUpdateQueue=>classComponentUpdater

Effect list

Side Effects:咱們能夠將React中的一個組件視爲一個使用state和props來計算UI的函數。每一個其餘活動,如改變DOM或調用生命週期方法,都應該被認爲是side-effects,react文檔中是這樣描述的side-effects的:

You’ve likely performed data fetching, subscriptions, or manually changing the DOM 的from React components before. We call these operations 「side effects」 (or 「effects」 for short) because they can affect other components and can’t be done during rendering.

React可以很是快速地更新,而且爲了實現高性能,它採用了一些有趣的技術。其中之一是構建帶有side-effects的fiber節點的線性列表,其具備快速迭代的效果。迭代線性列表比樹快得多,而且沒有必要在沒有side effects的節點上花費時間。

每一個fiber節點均可以具備與之相關的effects, 經過fiber節點中的effectTag字段表示。

此列表的目標是標記具備DOM更新或與其關聯的其餘effects的節點,此列表是WIP tree的子集,並使用nextEffect屬性,而不是current和workInProgress樹中使用的child屬性進行連接。

How it work

核心目標

  • 把可中斷的工做拆分紅多個小任務
  • 爲不一樣類型的更新分配任務優先級
  • 更新時可以暫停,終止,複用渲染任務

更新過程概述

咱們先看看其Fiber的更新過程,而後再針對過程當中的核心技術進行展開。

Reconciliation分爲兩個階段:reconciliation 和 commit

reconciliation

從圖中能夠看到,能夠把reconciler階段分爲三部分,分別以紅線劃分。簡單的歸納下三部分的工做:

  1. 第一部分從 ReactDOM.render() 方法開始,把接收的React Element轉換爲Fiber節點,併爲其設置優先級,記錄update等。這部分主要是一些數據方面的準備工做。
  2. 第二部分主要是三個函數:scheduleWork、requestWork、performWork,即安排工做、申請工做、正式工做三部曲。React 16 新增的異步調用的功能則在這部分實現。
  3. 第三部分是一個大循環,遍歷全部的Fiber節點,經過Diff算法計算全部更新工做,產出 EffectList 給到commit階段使用。這部分的核心是 beginWork 函數。

commit階段

這個階段主要作的工做拿到reconciliation階段產出的全部更新工做,提交這些工做並調用渲染模塊(react-dom)渲染UI。完成UI渲染以後,會調用剩餘的生命週期函數,因此異常處理也會在這部分進行

分配優先級

其上所列出的fiber結構中有個expirationTime。

expirationTime本質上是fiber work執行的優先級。

// 源碼中的priorityLevel優先級劃分
export const NoWork = 0;
// 僅僅比Never高一點 爲了保證連續必須完整完成
export const Never = 1;
export const Idle = 2;
export const Sync = MAX_SIGNED_31_BIT_INT;//整型最大數值,是V8中針對32位系統所設置的最大值
export const Batched = Sync - 1;
複製代碼

源碼中的computeExpirationForFiber函數,該方法用於計算fiber更新任務的最晚執行時間,進行比較後,決定是否繼續作下一個任務。

//爲fiber對象計算expirationTime
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  ...
  // 根據調度優先級計算ExpirationTime
    const priorityLevel = getCurrentPriorityLevel();
    switch (priorityLevel) {
      case ImmediatePriority:
        expirationTime = Sync;
        break;
        //高優先級 如由用戶輸入設計交互的任務
      case UserBlockingPriority:
        expirationTime = computeInteractiveExpiration(currentTime);
        break;
        // 正常的異步任務
      case NormalPriority:
        // This is a normal, concurrent update
        expirationTime = computeAsyncExpiration(currentTime);
        break;
      case LowPriority:
      case IdlePriority:
        expirationTime = Never;
        break;
      default:
        invariant(
          false,
          'Unknown priority level. This error is likely caused by a bug in ' +
            'React. Please file an issue.',
        );
    }
    ...
}

export const LOW_PRIORITY_EXPIRATION = 5000
export const LOW_PRIORITY_BATCH_SIZE = 250

export function computeAsyncExpiration(
  currentTime: ExpirationTime,
): ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    LOW_PRIORITY_EXPIRATION,
    LOW_PRIORITY_BATCH_SIZE,
  )
}

export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150
export const HIGH_PRIORITY_BATCH_SIZE = 100

export function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION,
    HIGH_PRIORITY_BATCH_SIZE,
  )
}

function computeExpirationBucket(
  currentTime,
  expirationInMs,
  bucketSizeMs,
): ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET -
    ceiling(
    // 以前的算法
     //currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
      MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )
  );
}
複製代碼
// 咱們把公式整理一下:
// low
 1073741821-ceiling(1073741821-currentTime+500,25) =>
 1073741796-((1073742321-currentTime)/25 | 0)*25
// high 
1073741821-ceiling(1073741821-currentTime+15,10)
複製代碼

簡單來講,最終結果是以25爲單位向上增長的,好比說咱們輸入102 - 126之間,最終獲得的結果都是625,可是到了127獲得的結果就是650了,這就是除以25取整的效果。

即計算出的React低優先級update的expirationTime間隔是25ms, React讓兩個相近(25ms內)的update獲得相同的expirationTime,目的就是讓這兩個update自動合併成一個Update,從而達到批量更新的目的。就像提到的doubleBuffer同樣,React爲提升性能,考慮得很是全面!

expiration算法源碼

推薦閱讀:jokcy大神解析=》expirationTime計算

執行優先級

那麼Fiber是如何作到異步實現不一樣優先級任務的協調執行的

這裏要介紹介紹瀏覽器提供的兩個API:requestIdleCallback和requestAnimationFrame:

requestIdleCallback: 在瀏覽器空閒時段內調用的函數排隊。是開發人員能夠在主事件循環上執行後臺和低優先級工做而不會影響延遲關鍵事件,如動畫和輸入響應。

其在回調參數中IdleDeadline能夠獲取到當前幀剩餘的時間。利用這個信息能夠合理的安排當前幀須要作的事情,若是時間足夠,那繼續作下一個任務,若是時間不夠就歇一歇。

requestAnimationFrame:告訴瀏覽器你但願執行一個動畫,而且要求瀏覽器在下次重繪以前調用指定的回調函數更新動畫

合做式調度:這是一種’契約‘調度,要求咱們的程序和瀏覽器緊密結合,互相信任。好比能夠由瀏覽器給咱們分配執行時間片,咱們要按照約定在這個時間內執行完畢,並將控制權還給瀏覽器。

Fiber所作的就是須要分解渲染任務,而後根據優先級使用API調度,異步執行指定任務:

  • 低優先級任務由requestIdleCallback處理,限制任務執行時間,以切分任務,同時避免任務長時間執行,阻塞UI渲染而致使掉幀。
  • 高優先級任務,如動畫相關的由requestAnimationFrame處理;

並非全部的瀏覽器都支持requestIdleCallback,可是React內部實現了本身的polyfill,因此沒必要擔憂瀏覽器兼容性問題。polyfill實現主要是經過rAF+postmessage實現的(最新版本去掉了rAF,有興趣的童鞋能夠看看=》SchedulerHostConfig

生命週期

由於其在協調階段任務可被打斷的特色,任務在切片後運行完一段便將控制權交還到react負責任務調度的模塊,再根據任務的優先級,繼續運行後面的任務。因此會致使某些組件渲染到一半便會打斷以運行其餘緊急,優先級更高的任務,運行完卻不會繼續以前中斷的部分,而是從新開始,因此在協調的全部生命週期都會面臨這種被屢次調用的狀況。
爲了限制這種被屢次重複調用,耗費性能的狀況出現,react官方一步步把處在協調階段的部分生命週期進行移除。

廢棄:

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps

新增:

newLifeCircle

爲何新的生命週期用static

static 是ES6的寫法,當咱們定義一個函數爲static時,就意味着沒法經過this調用咱們在類中定義的方法

經過static的寫法和函數參數,能夠感受React在和我說:請只根據newProps來設定derived state,不要經過this這些東西來調用幫助方法,可能會越幫越亂。用專業術語說:getDerivedStateFromProps應該是個純函數,沒有反作用(side effect)。

getDerivedStateFromError和componentDidCatch之間的區別是什麼?

簡而言之,由於所處階段的不一樣而功能不一樣。

getDerivedStateFromError是在reconciliation階段觸發,因此getDerivedStateFromError進行捕獲錯誤後進行組件的狀態變動,不容許出現反作用。

static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能夠顯降級 UI
    return { hasError: true };
}
複製代碼

componentDidCatch由於在commit階段,所以容許執行反作用。 它應該用於記錄錯誤之類的狀況:

componentDidCatch(error, info) {
    // "組件堆棧" 例子:
    //   in ComponentThatThrows (created by App)
    //   in ErrorBoundary (created by App)
    //   in div (created by App)
    //   in App
    logComponentStackToMyService(info.componentStack);
  }

複製代碼

生命週期相關資料點這裏=》生命週期

Suspense

Suspense的實現很詭異,也備受爭議。
用Dan的原話講:你將會恨死它,而後你會愛上他。

Suspense功能想解決從react出生到如今都存在的「異步反作用」的問題,並且解決得很是的優雅,使用的是「異步可是同步的寫法」.

Suspense暫時只是用於搭配lazy進行代碼分割,在組件等待某事時「暫停」渲染的能力,並顯示加載的loading,但他的做用遠遠不止如此,當下在concurrent mode實驗階段文檔下提供了一種suspense處理異步請求獲取數據的方法。

用法

// 懶加載組件切換時顯示過渡組件
const ProfilePage = React.lazy(() => import('./ProfilePage')); // Lazy-loaded

// Show a spinner while the profile is loading
<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>
複製代碼
// 異步獲取數據
import { unstable_createResource } from 'react-cache'

const resource = unstable_createResource((id) => {
  return fetch(`/demo/${id}`)
})

function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <ProfileTimeline />
      </Suspense>
    </Suspense>
  );
}

function ProfileDetails() {
  // Try to read user info, although it might not have loaded yet
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

function ProfileTimeline() {
  // Try to read posts, although they might not have loaded yet
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}
複製代碼
  • 在render函數中,咱們能夠寫入一個異步請求,請求數據
  • react會從咱們緩存中讀取這個緩存
  • 若是有緩存了,直接進行正常的render
  • 若是沒有緩存,那麼會拋出一個異常,這個異常是一個promise
  • 當這個promise完成後(請求數據完成),react會繼續回到原來的render中(其實是從新執行一遍render),把數據render出來
  • 徹底同步寫法,沒有任何異步callback之類的東西

若是你尚未明白這是什麼意思那我簡單的表述成下面這句話:

調用render函數->發現有異步請求->懸停,等待異步請求結果->再渲染展現數據

看着是很是神奇的,用同步方法寫異步,並且沒有yield/async/await,簡直能把人看傻眼了。這麼作的好處天然就是,咱們的思惟邏輯很是的簡單,清楚,沒有callback,沒有其餘任何玩意,不能不說,看似優雅了很是多並且牛逼。

官方文檔指出它還將提供官方的方法進行數據獲取

原理

看一下react提供的unstable_createResource源碼

export function unstable_createResource(fetch, maybeHashInput) {
  const resource = {
    read(input) {
      ...
      const result = accessResult(resource, fetch, input, key);
      switch (result.status) {
        // 還未完成直接拋出自身promise
        case Pending: {
          const suspender = result.value;
          throw suspender;
        }
        case Resolved: {
          const value = result.value;
          return value;
        }
        case Rejected: {
          const error = result.value;
          throw error;
        }
        default:
          // Should be unreachable
          return (undefined: any);
      }
    },
  };
  return resource;
}
複製代碼

爲此,React使用Promises。 組件能夠在其render方法(或在組件的渲染過程當中調用的任何東西,例如新的靜態getDerivedStateFromProps)中拋出Promise。 React捕獲了拋出的Promise,並在樹上尋找最接近的Suspense組件,Suspense其自己具備componentDidCatch,將promise當成error捕獲,等待其執行完成其更改狀態從新渲染子組件。

Suspense組件將一個元素(fallback 做爲其後備道具,不管子節點在何處或爲何掛起,都會在其子樹被掛起時進行渲染。

如何達成異常捕獲

  1. reconciliation階段的 renderRoot 函數,對應異常處理方法是 throwException
  2. commit階段的 commitRoot 函數,對應異常處理方法是 dispatch

reconciliation階段的異常捕獲

react-reconciler中的performConcurrentWorkOnRoot

// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
// 這裏是每個經過Scheduler的concurrent任務的入口
function performConcurrentWorkOnRoot(root, didTimeout) {
    ...
    do {
        try {
            //開始執行Concurrent任務直到Scheduler要求咱們讓步
            workLoopConcurrent();
            break;
        } catch (thrownValue) {
            handleError(root, thrownValue);
        }
    } while (true);
    ...
}

function handleError(root, thrownValue) {
    ...
      throwException(
        root,
        workInProgress.return,
        workInProgress,
        thrownValue,
        renderExpirationTime,
      );
      workInProgress = completeUnitOfWork(workInProgress);
   ...
}
複製代碼

throwException

do {
    switch (workInProgress.tag) {
      ....
      case ClassComponent:
        // Capture and retry
        const errorInfo = value;
        const ctor = workInProgress.type;
        const instance = workInProgress.stateNode;
        if (
          (workInProgress.effectTag & DidCapture) === NoEffect &&
          (typeof ctor.getDerivedStateFromError === 'function' ||
            (instance !== null &&
              typeof instance.componentDidCatch === 'function' &&
              !isAlreadyFailedLegacyErrorBoundary(instance)))
        ) {
          workInProgress.effectTag |= ShouldCapture;
          workInProgress.expirationTime = renderExpirationTime;
          // Schedule the error boundary to re-render using updated state
          const update = createClassErrorUpdate(
            workInProgress,
            errorInfo,
            renderExpirationTime,
          );
          enqueueCapturedUpdate(workInProgress, update);
          return;
        }
    }
    ...
}
    
複製代碼

throwException函數分爲兩部分 一、遍歷當前異常節點的全部父節點,找到對應的錯誤信息(錯誤名稱、調用棧等),這部分代碼在上面中沒有展現出來

二、第二部分是遍歷當前異常節點的全部父節點,判斷各節點的類型,主要仍是上面提到的兩種類型,這裏重點講ClassComponent類型,判斷該節點是不是異常邊界組件(經過判斷是否存在componentDidCatch生命週期函數等),若是是找到異常邊界組件,則調用 createClassErrorUpdate函數新建update,並將此update放入此節點的異常更新隊列中,在後續更新中,會更新此隊列中的更新工做

commit階段

ReactFiberWorkLoop中的finishConcurrentRender=》 commitRoot=》 commitRootImpl=》captureCommitPhaseError

commit被分爲幾個子階段,每一個階段都try catch調用了一次captureCommitPhaseError

  1. 突變(mutate)前階段:咱們在突變前先讀出主樹的狀態,getSnapshotBeforeUpdate在這裏被調用
  2. 突變階段:咱們在這個階段更改主樹,完成WIP樹轉變爲current樹
  3. 樣式階段:調用從被更改後主樹讀取的effect
export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
  if (sourceFiber.tag === HostRoot) {
    // Error was thrown at the root. There is no parent, so the root
    // itself should capture it.
    captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error);
    return;
  }

  let fiber = sourceFiber.return;
  while (fiber !== null) {
    if (fiber.tag === HostRoot) {
      captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error);
      return;
    } else if (fiber.tag === ClassComponent) {
      const ctor = fiber.type;
      const instance = fiber.stateNode;
      if (
        typeof ctor.getDerivedStateFromError === 'function' ||
        (typeof instance.componentDidCatch === 'function' &&
          !isAlreadyFailedLegacyErrorBoundary(instance))
      ) {
        const errorInfo = createCapturedValue(error, sourceFiber);
        const update = createClassErrorUpdate(
          fiber,
          errorInfo,
          // TODO: This is always sync
          Sync,
        );
        enqueueUpdate(fiber, update);
        const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
        if (root !== null) {
          ensureRootIsScheduled(root);
          schedulePendingInteractions(root, Sync);
        }
        return;
      }
    }
    fiber = fiber.return;
  }
}
複製代碼

captureCommitPhaseError函數作的事情和上部分的 throwException 相似,遍歷當前異常節點的全部父節點,找到異常邊界組件(有componentDidCatch生命週期函數的組件),新建update,在update.callback中調用組件的componentDidCatch生命週期函數。

細心的小夥伴應該注意到,throwException 和 captureCommitPhaseError在遍歷節點時,是從異常節點的父節點開始遍歷,因此異常捕獲通常由擁有componentDidCatch或getDerivedStateFromError的異常邊界組件進行包裹,而其是沒法捕獲並處理自身的報錯。

Hook相關

Function Component和Class Component

Class component 劣勢

  1. 狀態邏輯難複用:在組件之間複用狀態邏輯很難,可能要用到 render props (渲染屬性)或者 HOC(高階組件),但不管是渲染屬性,仍是高階組件,都會在原先的組件外包裹一層父容器(通常都是 div 元素),致使層級冗餘 趨向複雜難以維護:
  2. 在生命週期函數中混雜不相干的邏輯(如:在 componentDidMount 中註冊事件以及其餘的邏輯,在 componentWillUnmount 中卸載事件,這樣分散不集中的寫法,很容易寫出 bug ) 類組件中處處都是對狀態的訪問和處理,致使組件難以拆分紅更小的組件
  3. this 指向問題:父組件給子組件傳遞函數時,必須綁定 this

可是在16.8以前react的函數式組件十分羸弱,基本只能做用於純展現組件,主要由於缺乏state和生命週期。

hooks優點

  • 能優化類組件的三大問題
  • 能在無需修改組件結構的狀況下複用狀態邏輯(自定義 Hooks )
  • 能將組件中相互關聯的部分拆分紅更小的函數(好比設置訂閱或請求數據)
  • 反作用的關注點分離:反作用指那些沒有發生在數據向視圖轉換過程當中的邏輯,如 ajax 請求、訪問原生dom 元素、本地持久化緩存、綁定/解綁事件、添加訂閱、設置定時器、記錄日誌等。以往這些反作用都是寫在類組件生命週期函數中的。而 useEffect 在所有渲染完畢後纔會執行,useLayoutEffect 會在瀏覽器 layout 以後,painting 以前執行。

capture props和capture value特性

capture props

class ProfilePage extends React.Component {
  showMessage = () => {
    alert("Followed " + this.props.user);
  };

  handleClick = () => {
    setTimeout(this.showMessage, 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}
複製代碼
function ProfilePage(props) {
  const showMessage = () => {
    alert("Followed " + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return <button onClick={handleClick}>Follow</button>;
}
複製代碼

這兩個組件都描述了同一個邏輯:點擊按鈕 3 秒後 alert 父級傳入的用戶名。

那麼 React 文檔中描述的 props 不是不可變(Immutable) 數據嗎?爲啥在運行時還會發生變化呢?

緣由在於,雖然 props 不可變,是 this 在 Class Component 中是可變的,所以 this.props 的調用會致使每次都訪問最新的 props。

無可厚非,爲了在生命週期和render重能拿到最新的版本react自己會實時更改this,這是this在class組件的本職。

這揭露了關於用戶界面的有趣觀察,若是咱們說ui從概念上是一個當前應用狀態的函數,事件處理就是render結果的一部分,咱們的事件處理屬於擁有特定props或state的render。每次 Render 的內容都會造成一個快照並保留下來,所以當狀態變動而 Rerender 時,就造成了 N 個 Render 狀態,而每一個 Render 狀態都擁有本身固定不變的 Props 與 State。

然而在setTimeout的回調中獲取this.props會打斷這種的關聯,失去了與某一特定render綁定,因此也失去了正確的props。

而 Function Component 不存在 this.props 的語法,所以 props 老是不可變的。

測試地址

hook中的capture value

function MessageThread() {
  const [message, setMessage] = useState("");

  const showMessage = () => {
    alert("You said: " + message);
  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = e => {
    setMessage(e.target.value);
  };

  return (
    <>
      <input value={message} onChange={handleMessageChange} />
      <button onClick={handleSendClick}>Send</button>
    </>
  );
}
複製代碼

hook重一樣有capture value,每次渲染都有本身的 Props and State,若是要時刻獲取最新的值,規避 capture value 特性,能夠用useRef

const lastest = useRef("");

const showMessage = () => {
    alert("You said: " + lastest.current);
};

const handleSendClick = () => {
    setTimeout(showMessage, 3000);
};

const handleMessageChange = e => {
    lastest.current = e.target.value;
};
複製代碼

測試地址

Hooks實現原理

在上面fiber結構分析能夠看出如今的Class component的state和props是記錄在fiber上的,在fiber更新後纔會更新到component的this.state和props裏面,而並非class component本身調理的過程。這也給了實現hooks的方便,由於hooks是放在function component裏面的,他沒有本身的this,但咱們自己記錄state和props就不是放在class component this上面,而是在fiber上面,因此咱們有能力記錄狀態以後,也有能力讓function component更新過程中拿到更新以後的state。

React 依賴於 Hook 的調用順序

平常調用三次

function Form() {
  const [hero, setHero] = useState('iron man');
  if(hero){
    const [surHero, setSurHero] = useState('Captain America');
  }
  const [nbHero, setNbHero] = useState('hulk');
  // ...
}
複製代碼

來看看咱們的useState是怎麼實現的

// useState 源碼中的鏈表實現
import React from 'react';
import ReactDOM from 'react-dom';

let firstWorkInProgressHook = {memoizedState: null, next: null};
let workInProgressHook;

function useState(initState) {
    let currentHook = workInProgressHook.next ? workInProgressHook.next : {memoizedState: initState, next: null};

    function setState(newState) {
        currentHook.memoizedState = newState;
        render();
    }
	
	// 假如某個 useState 沒有執行,會致使Next指針移動出錯,數據存取出錯
    if (workInProgressHook.next) {
        // 這裏只有組件刷新的時候,纔會進入
        // 根據書寫順序來取對應的值
        // console.log(workInProgressHook);
        workInProgressHook = workInProgressHook.next;
    } else {
        // 只有在組件初始化加載時,纔會進入
        // 根據書寫順序,存儲對應的數據
        // 將 firstWorkInProgressHook 變成一個鏈表結構
        workInProgressHook.next = currentHook;
        // 將 workInProgressHook 指向 {memoizedState: initState, next: null}
        workInProgressHook = currentHook;
        // console.log(firstWorkInProgressHook);
    }
    return [currentHook.memoizedState, setState];
}

function Counter() {
    // 每次組件從新渲染的時候,這裏的 useState 都會從新執行
    const [name, setName] = useState('計數器');
    const [number, setNumber] = useState(0);
    return (
        <>
            <p>{name}:{number}</p>
            <button onClick={() => setName('新計數器' + Date.now())}>新計數器</button>
            <button onClick={() => setNumber(number + 1)}>+</button>
        </>
    )
}

function render() {
    // 每次從新渲染的時候,都將 workInProgressHook 指向 firstWorkInProgressHook
    workInProgressHook = firstWorkInProgressHook;
    ReactDOM.render(<Counter/>, document.getElementById('root'));
}

render();
複製代碼

咱們來還原一下這個過程 你們看完應該瞭解,當下設置currentHook實際上是上個workInProgressHook經過next指針進行綁定獲取的,因此若是在條件語句中打破了調用順序,將會致使next指針指向出現誤差,這個時候你傳進去的setState是沒法正確改變對應的值,由於

各類自定義封裝的hooks =》react-use

爲何順序調用對 React Hooks 很重要?

THE END

第二次在掘金上發文,小陳也是react小菜🐔,但願能跟你們一塊兒討論學習,向高級前端架構進階!讓咱們一塊兒愛上fiber

參考:

如何以及爲何React Fiber使用鏈表遍歷組件樹
React Fiber架構
React 源碼解析 - reactScheduler 異步任務調度
展望 React 17,回顧 React 往事 全面 深刻
這多是最通俗的 React Fiber(時間分片) 打開方式=>調度策略
全面瞭解 React 新功能: Suspense 和 Hooks 生命週期
詳談 React Fiber 架構(1)

相關文章
相關標籤/搜索