React Fiber 架構理解

React Fiber 架構理解

引用原文: React Fiber Architecture

React Fiber is an ongoing reimplementation of React's core algorithm. It is the culmination of over two years of research by the React team.javascript

The goal of React Fiber is to increase its suitability for areas like animation, layout,and gestures. Its headline feature is incremental rendering: the ability to split rendering work into chunks and spread it out over multiple frames.html

Other key features include the ability to pause, abort, or reuse work as new updates come in; the ability to assign priority to different types of updates; and new concurrency primitives.java

React Fibre 是 React 核心算法正在進行的從新實現。它是 React 團隊兩年多的研究成果。react

React Fiber 的目標是提升其對動畫,佈局和手勢等領域的適用性。它的主體特徵是增量渲染:可以將渲染工做分割成塊,並將其分散到多個幀中。git

其餘主要功能包括在進行更新時暫停,停止或從新使用工做的能力,爲不一樣類型的更新分配優先權的能力和新的併發原語。github

React16以前組件的渲染邏輯

先來看一下react組件渲染時經歷的生命週期:web

掛載階段:算法

  • constructor()
  • componentWillMount()
  • render()
  • componentDidMount()

更新階段:npm

  • componentWillReceiveProps()
  • shouldComponentUpdate()
  • componentWillUpdate()
  • render()
  • componentDidUpdate

卸載階段:redux

  • componentWillUnmount()

在以前的版本中,若是你實現一個很複雜的深度嵌套的複合組件,會出現下面的狀況:

現有層級關係以下的四個組件:

圖片描述

組件渲染時調用的生命週期順序:

圖片描述

上圖展現的是A,B,C,D的掛載階段調用的生命週期渲染順序,能夠看到從頂層組件開始調用各生命週期,一直向下,直至調用完最底層子組件的生命週期。而後再向上調用。
組件更新階段同理。

組件掛載以後,假如修改最上層組件的數據(state),組件更新時的調用棧:

圖片描述

若是這是一個很大,層級很深的組件,能夠想像到,組件在渲染時,調用棧過長,再加上若是在期間進行了各類複雜的操做,就可能致使長時間阻塞主線程,react渲染它須要幾十甚至幾百毫秒,這樣的話react就會一直佔用瀏覽器主線程,任何其餘的操做(包括用戶的點擊,鼠標移動等操做)都沒法執行,帶來很是很差的用戶體驗。

React Fiber的出現

React Fiber 就是爲了解決上面的問題而生。

好似一個潛水員,當它一頭扎進水裏,就要往最底層一直遊,直到找到最底層的組件,而後他再上岸。 在這期間,岸上發生的任何事,都不能對他進行干擾,若是有更重要的事情須要他去作(如用戶操做),也必須得等他上岸。

Fiber 本質上是一個虛擬的堆棧幀,新的調度器會按照優先級自由調度這些幀,從而將以前的同步渲染改爲了異步渲染,在不影響體驗的狀況下去分段計算更新。它讓潛水員會每隔一段時間就上岸,看是否有更重要的事情要作。

對於如何區別優先級,React 有本身的一套邏輯。對於動畫這種實時性很高的東西,也就是 16 ms 必須渲染一次保證不卡頓的狀況下,React 會每 16 ms(之內) 暫停一下更新,返回來繼續渲染動畫。

React Fiber 架構

調度拆分爲小任務

瀏覽器自己也不斷進化中,隨着頁面由簡單的展現轉向WebAPP,它須要一些新能力來承載更多節點的展現與更新。
下面是一些自救措施:

  • requestAnimationFrame
  • requestIdleCallback
  • web worker
  • IntersectionObserver

react官方採用的是 requestIdleCallback,爲了兼容全部平臺,facebook 單獨實現了其功能,做爲一個獨立的 npm 包使用 react-schedule

其做用是會在瀏覽器空閒時期依次調用函數, 這就能夠在主事件循環中執行後臺或低優先級的任務,並且不會對像動畫和用戶交互這樣延遲觸發並且關鍵的事件產生影響。函數通常會按先進先調用的順序執行,除非函數在瀏覽器調用它以前就到了它的超時時間。

簡化後的大體流程圖以下:

圖片描述

Fiber Node 及 Fiber Tree

  • 從流程圖上看到會有 Fiber Node 節點,這個是在 react 生成的 Virtual Dom 基礎上增長的一層數據結構,主要是爲了將遞歸遍歷轉變成循環遍歷,配合 requestIdleCallback API, 實現任務拆分、中斷與恢復。爲了實現循環遍歷,Fiber Node 上攜帶了更多的信息。
  • 每個 Fiber Node 節點與 Virtual Dom 一一對應,全部 Fiber Node 鏈接起來造成 Fiber tree, 是個單鏈表樹結構

兩個階段:reconciliationcommit

對於異步渲染,如今渲染有兩個階段:reconciliationcommit 。前者過程是能夠打斷的,後者不能暫停,會一直更新界面直到完成。

reconciliation 處理過程

  • 當執行 setState() 或首次 render() 時,進入工做循環,循環體中處理的單元爲 Fiber Node, 便是拆分任務的最小單位,從根節點開始,自頂向下逐節點構造 workInProgress tree(構建中的新 Fiber Tree)。
  • 每一個工做處理單元作的事情,由 beginWork(), completeUnitOfWork() 兩部分構成。
  • beginWork()主要作的事情是從頂向下生成全部的 Fiber Node,並標記 Diff, 不包括兄弟節點,每一個 Fiber Node 的處理過程根據組件類型略有差別,以 ClassComponent 爲例:

1 若是當前節點不須要更新,直接把子節點clone過來,要更新的話標記更新類型
2 更新當前節點狀態(props, state, context等)
3 調用shouldComponentUpdate()
4 調用組件實例方法 render() 得到新的子節點,併爲子節點建立 Fiber Node(建立過程會盡可能複用現有 Fiber Node,子節點增刪也發生在這裏)
5 若是沒有產生 child fiber,進入下一階段 completeUnitOfWork

  • completeUnitOfWork() 當沒有子節點,開始遍歷兄弟節點做爲下一個處理單元,處理完兄弟節點開始向上回溯,直到再次回去根節點爲止,將收集向上回溯過程當中的全部 diff,拿到 diff 後開始進入 commit 階段。
  • 構建 workInProgress tree 的過程就是 diff 的過程,經過 requestIdleCallback 來調度執行一組任務,每完成一個任務後回來看看有沒有插隊的(更緊急的),把時間控制權交還給主線程,直到下一次 requestIdleCallback 回調再繼續構建workInProgress tree。

兩個階段涉及到的生命週期:

Reconciliation 階段 (React算法,用來比較2顆樹,以肯定哪些部分須要從新渲染

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

Commit 階段 (用於呈現React應用的數據更改。一般是setState的結果。最終致使從新渲染。

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

由於 reconciliation 階段是能夠被打斷的,因此 reconciliation 階段會執行的生命週期函數就可能會出現調用屢次的狀況,從而引發 Bug。因此對於 reconciliation 階段調用的幾個函數,除了 shouldComponentUpdate 之外,其餘都應該避免去使用,而且 React16 中也引入了新的 API 來解決這個問題。

因而官方推出了getDerivedStateFromProps,讓你在render設置新state,你主要返回一個新對象,它就主動幫你setState。因爲這是一個靜態方法,你不能取到 this,固然你也不能操做instance,這就阻止了你屢次操做setState。這樣一來,getDerivedStateFromProps的邏輯應該會很簡單,這樣就不會出錯,不會出錯,就不會打斷DFS過程。

getDerivedStateFromProps取代了原來的componentWillMountcomponentWillReceiveProps方法,該函數會在組件 初始化 和 更新 時被調用

class ExampleComponent extends React.Component {
  // Initialize state in constructor,
  // Or with a property initializer.
  state = {};

  static getDerivedStateFromProps(nextProps, prevState) {
    if (prevState.someMirroredValue !== nextProps.someValue) {
      return {
        derivedData: computeDerivedState(nextProps),
        someMirroredValue: nextProps.someValue
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

在進入commi階段時,組件多了一個新鉤子叫getSnapshotBeforeUpdate,它與commit階段的鉤子同樣只執行一次。
getSnapshotBeforeUpdate 用於替換 componentWillUpdate ,該函數會在 update 後 DOM 更新前被調用,用於讀取最新的 DOM 數據。

因而整個流程變成這樣:(引用大神@司徒正美的圖)

React16 生命週期函數用法建議

結合 React Fiber 架構 建議以下使用react生命週期

class ExampleComponent extends React.Component {
  // 用於初始化 state
  constructor() {}
  // 用於替換 `componentWillReceiveProps` ,該函數會在初始化和 `update` 時被調用
  // 由於該函數是靜態函數,因此取不到 `this`
  // 若是須要對比 `prevProps` 須要單獨在 `state` 中維護
  static getDerivedStateFromProps(nextProps, prevState) {}
  // 判斷是否須要更新組件,多用於組件性能優化
  shouldComponentUpdate(nextProps, nextState) {}
  // 組件掛載後調用
  // 能夠在該函數中進行請求或者訂閱
  componentDidMount() {}
  // 用於得到最新的 DOM 數據
  getSnapshotBeforeUpdate() {}
  // 組件即將銷燬
  // 能夠在此處移除訂閱,定時器等等
  componentWillUnmount() {}
  // 組件銷燬後調用
  componentDidUnMount() {}
  // 組件更新後調用
  componentDidUpdate() {}
  // 渲染組件函數
  render() {}
  // 如下函數不建議使用
  UNSAFE_componentWillMount() {}
  UNSAFE_componentWillUpdate(nextProps, nextState) {}
  UNSAFE_componentWillReceiveProps(nextProps) {}
}

16 大版本主要更新還解決如下痛點:

  • 組件不能返回數組,最見的場合是UL元素下只能使用LI,TR元素下只能使用TD或TH,這時這裏有一個組件循環生成LI或TD列表時,咱們並不想再放一個DIV,這會破壞HTML的語義。
  • 彈窗問題,以前一直使用不穩定的unstable_renderSubtreeIntoContainer。彈窗是依賴原來DOM樹的上下文,所以這個API第一個參數是組件實例,經過它獲得對應虛擬DOM,而後一級級往上找,獲得上下文。它的其餘參數也很好用,但這個方法一直沒有轉正。。。
  • 異常處理,咱們想知道哪一個組件出錯,雖然有了React DevTool,可是太深的組件樹查找起來仍是很吃力。但願有個方法告訴我出錯位置,而且出錯時能讓我有機會進行一些修復工做
  • HOC的流行帶來兩個問題,畢竟是社區興起的方案,沒有考慮到ref與context的向下傳遞。
  • 組件的性能優化全憑人肉,而且主要集中在SCU,但願框架能幹些事情,即便不用SCU,性能也能上去。

新特性:

  • render / 純組件可以 return 任何數據結構
  • CreatePortal API,更好的處理 Dialog 這種場景組件
  • 新的 context api,嘗試代替一部分 redux 的職責
  • 異步渲染/時間切片(time slicing),成倍提升性能
  • componentDidCatch,錯誤邊界,框架層面上提升用戶 debug 的能力
  • 網絡請求 IO(Suspense),更好的處理異步網絡 IO

參考資料:

相關文章
相關標籤/搜索