理解 React Fiber & Concurrent Mode

拋出問題

React 目前主要的問題有兩個html

react-cpu-io.jpg

  1. 頁面在大量 DOM 節點同時更新的狀況下,會出現延遲很嚴重的現象,具體表現爲交互/渲染的卡頓現象
  2. 異步操做(好比 data fetching)須要在組件 mount 以後進行,致使子組件的 fetching 有可能被父組件的 fetching 阻塞(下一篇會細講)

其中第一個問題會在設備計算能力差(好比 mobile device)的狀況下被放大,咱們能夠把這個問題和 CPU 綁在一塊兒(CPU-bound)react

第二個問題會在設備網絡狀況差(好比 3G slow network)的狀況下被放大,咱們能夠把這個問題和 IO 綁在一塊兒(IO-bound)git

今天咱們主要來說下,React concurrent mode 是如何解決第一個問題的,第二個問題會在後面的代碼實戰文章再仔細講下。github

分析問題

首先,這個問題並非一個 performance 問題,而是一個調度 (scheduling) 問題,因此提升處理速度並不能徹底解決這個問題,就像提升網速並不能解決 HTTP/1.1 協議的線頭阻塞 (Head-of-line blocking) 問 題。瀏覽器

no-debounce.gif

這個問題的本質是,瀏覽器的 main thread 是單線程的,短期大量 CPU consuming 的 task 被加到了 call stack,致使 event loop 在好幾個週期都沒有空閒去處理 queue 裏面的用戶交互 (click, scroll, etc.),從而用戶感知到了卡頓。網絡

react-event-loop.gif

這兩個問題在目前能不能解決呢?其實是能夠的,可是解決的方式不夠優雅,或者說,沒有徹底解決。好比第一個問題在 Table 搜索的時候很常見,若是 Table 數據特別多,搜索的時候的卡頓是必定的。解決方法也很簡單,使用 debounce 函數在用戶連續輸入的時候,不進行操做,等用戶輸入暫停再搜索。這種 workaround 至關於解決了一半的問題,用戶能接受最好,不能接受,其實也沒有別的更好的方法了。異步

debounce.gif

解決問題 - React Fiber

解決這個問題的原理其實很簡單,HTTP/2 採用分幀 (Frame) 的方式解決的 HTTP/1.1 的線頭阻塞問題,咱們也能夠將大量的更新任務 (Batch update task) 拆分紅一個個小的 task,從而讓 event loop 有機會處理用戶交互的 callback。async

http2-frame.jpg

React Fiber 就是 React Team 一直在持續開發用來解決上面問題的新 algorithm 或者叫 reconciler。ide

Fiber 的原理挺複雜的,簡單說就是原來須要一口氣放到 call stack 裏面執行的一堆任務,被放到了 heap 裏面由一個 scheduler 來調度執行。函數

react-concurrent-mode-img.jpg

其實 React Team 應該在一開始就意識到這個問題了,因此 setState() 被設計成了異步的,但就像官方文檔裏說的,他們並無徹底利用 asynchronous update (not taking advantage of this) 。僅僅異步是不夠的,blocking 仍是會 blocking,只有 scheduling the update 才能實現可中斷渲染 (interruptible rendering)

React Concurrent Mode

react-concurrent-mode-demo.gif

Concurrent Mode 指的就是 React 利用上面 Fiber 帶來的新特性的開啓的新模式 (mode)。目前 React 實驗版本容許用戶選擇三種 mode

  • Legacy Mode: 就至關於目前穩定版的模式
  • Blocking Mode: 應該是之後會代替 Legacy Mode 而長期存在的模式
  • Concurrent Mode: 之後會變成 default 的模式

Concurrent Mode 其實開啓了一堆新特性,其中有兩個最重要的特性能夠用來解決咱們開頭提到的兩個問題

  • Suspense
  • useTrasition

其中 Suspense 能夠用來解決請求阻塞的問題,UI 卡頓的問題其實開啓 concurrent mode 就已經解決的,但如何利用 concurrent mode 來實現更友好的交互仍是須要對代碼作一番改動的,後面的文章會用實際代碼的方式講下這兩個的使用,這裏給一個大數據下的不一樣 mode 下的頁面更新速度的對照 demo

須要注意

上面 Fiber 的 update 機制,有個細節問題就是,既然如今的更新是能夠被打斷的,那若是打斷的操做影響了更新怎麼辦?好比說有個 background-color 我原來要變成綠色,可是用戶點擊了一個按鈕,background-color 要變成紅色,那最終結果會怎樣?指望確定是紅色,否則就是 bug 了。所以 React 這裏作了處理,更新到一半的 work 是能夠被丟棄而從新開始的。

這樣的作法沒錯,可是就帶來一個問題,有些生命週期函數可能會被屢次調用

react-lifecycle-methods.jpg

咱們能夠將全部的生命週期函數分紅兩類,分別運行在 Render 階段和 Commit 階段

  • Render phase 簡單說就是決定究竟渲染什麼的階段,涉及絕大多數生命週期。

  • Commit phase 簡單說就是將須要渲染的內容推到 DOM (若是是 DOM 環境) 的階段。

componentDidMount(), componentDidUpdate(), componentWillUnmount() 這三個生命週期函數 (lifecycle method) 運行在 commit 階段,其他的生命週期函數均可以簡單理解爲運行在 render 階段。

咱們上面說更新到一半的 work 有可能被從新啓動,這就意味着,像是 rendershouldComponentUpdate 等等這些在 render 階段的生命週期函數並非像你指望的那樣只執行一個,而是有可能被執行屢次。

解決方式有兩種:

  1. 把在這些生命週期執行的邏輯放到 commit phase 執行,若是能徹底作到,這裏能夠直接將組件改爲 React hook 的形式。
  2. 保證 render phase 執行的邏輯是冪等的 (idempotent),不理解的能夠查下,這個很簡單很少講

總結

React Concurrent Mode 經過啓用 Fiber reconciler 實現了可中斷渲染 (interruptible render),從而讓一系列的交互優化變成了可能。下篇文章會使用實際代碼來說解究竟如何正確使用 Concurrent Mode API

Ref

相關文章
相關標籤/搜索