React18,不遠啦?

React前不久的一次PR #21488中,核心成員Brian VaughnReact內一些API、以及內部flag做出調整。html

其中最引人注目的改動是:React入口增長createRoot API前端

業界將這一變化解讀爲:Concurrent Mode(後文簡稱爲CM)將在不久後穩定,並出如今正式版中。react

React17是一個過渡版本,用以穩定CM。一旦CM穩定,那v18的進度會大大加快。git

能夠說從18年到21年,React團隊的主要工做就是圍繞CM展開的,那麼:github

  • CM是什麼?
  • CM能解決React什麼問題?
  • 爲何經歷快4年,跨越1六、17兩個版本,CM還不穩定?

本文將做出解答。算法

CM是什麼

要了解CM(併發模式)是什麼,首先須要知道React源碼的運行流程。瀏覽器

React大致能夠分爲兩個工做階段:網絡

  • render階段

render階段會計算一次更新中變化的部分(經過diff算法),因組件的render函數在該階段調用而得名。併發

render階段可能是異步的(取決於觸發更新的場景)。app

  • commit階段

commit階段會將render階段計算的須要變化的部分渲染在視圖中。對應ReactDOM來講會執行appendChildremoveChild等。

commit階段必定是同步調用(這樣用戶不會看到渲染不徹底的UI

咱們經過ReactDOM.render建立的應用屬於legacy模式。

在該模式下一次render階段對應一次commit階段。

若是咱們經過ReactDOM.createRoot(當前穩定版本中尚未此API)建立的應用屬於開篇提到的CMconcurrent模式)

CM下,更新有了優先級的概念,render階段可能被高優先級的更新打斷。

因此render階段可能會重複屢次(被打斷後從新開始)。

可能屢次render階段對應一次commit階段。

此外,還有個blocking模式用於方便開發者慢慢從legacy模式過渡到CM

你能夠從特性對比看到不一樣模式支持的特性:

不一樣模式支持的特性

爲何須要CM?

知道了CM是什麼,那麼他有什麼用?爲何React核心團隊會耗時3年多(18年開始)來實現他?

這得從React的設計理念聊起。

咱們能夠從官網React哲學看到React的設計理念:

咱們認爲, React是用 JavaScript構建 快速響應的大型 Web應用程序的首選方式。

其中快速響應是重點。

那麼什麼影響快速響應呢?React團隊給出的答案:

CPU的瓶頸和 IO的瓶頸

CPU的瓶頸

考慮以下demo,咱們渲染3000的列表項:

function App() {
  const len = 3000;
  return (
    <ul>
      {Array(len).fill(0).map((_, i) => <li>{i}</li>)}
    </ul>
  );
}

const rootEl = document.querySelector("#root");
ReactDOM.render(<App/>, rootEl);

剛纔說過,在legacy模式下render階段不會被打斷,則這3000個lirender都得在同一個瀏覽器宏任務中完成。

長時間的計算會阻塞線程,形成頁面掉幀,這就是CPU的瓶頸。

解決的辦法就是:啓用CM,將render階段變爲可中斷的,

當瀏覽器一幀剩餘時間很少時將控制權交給瀏覽器。等下一幀的空餘時間再繼續組件render

IO的瓶頸

除了長時間計算致使的卡頓,網絡請求時的loading狀態也會形成頁面不可交互,這就是IO的瓶頸。

IO瓶頸是客觀存在的。

做爲前端,能作的只能是儘早請求須要的數據。

可是,一般狀況下:代碼可維護性請求效率是相悖的。

什麼意思呢,舉個例子:

假設咱們封裝了請求數據的方法useFetch,經過返回值是否存在區分是否請求到數據。

function App() {
  const data = useFetch();
  
  return {data ? <User data={data}/> : null};
}

爲了提升代碼可維護性useFetch與要渲染的組件User存在於同一個組件App中。

然而,若是User組件內還須要進一步請求數據呢(以下profile數據)?

function User({data}) {
  const {id, name} = data?.id || {};
  const profile = useFetch(id);
  
  return (
    <div>
      <p>{name}</p>
      {profile ? <Profile data={profile} /> : null}
    </div>
  )
}

本着代碼可維護性原則,useFetch與要渲染的組件Profile存在於同一個組件User中。

可是,這樣組織代碼,Profile組件只能等User render後再render

數據只能像瀑布的水同樣,一層一層流下來。

這種低效的請求數據方式被稱爲waterfall

爲了提升請求效率,咱們能夠將「請求Profile組件所需數據的操做」提到App組件內,合併在useFetch中:

function App() {
  const data = useFetch();
  
  return {data ? <User data={data}/> : null};
}

可是這樣就下降了代碼可維護性Profile組件離profile數據太遠)。

React團隊從Relay團隊借鑑經驗,藉助Suspense特性,提出了Server Components

就是爲了在處理IO瓶頸時兼顧代碼可維護性請求效率

這一特性的實現須要CM更新有不一樣優先級

CM爲何花費這麼久?

接下來,咱們從源碼特性生態三個方面,自底向上看看CM的普及有多麼不容易。

源碼層面

優先級算法改造

在v16.13以前,React已經實現了基本的CM功能。

咱們以前聊過,CM有更新優先級的概念。以前是經過一個毫秒數expirationTime標記更新的過時時間。

  • 經過對比不一樣更新的expirationTime判斷優先級高低
  • 經過對比更新的expirationTime與當前時間判斷更新是否過時(過時須要同步執行)

可是,expirationTime做爲一個與時間相關的浮點數,沒法表示一批優先級這個概念。

爲了實現更上層的Server Components特性,須要有一批優先級這個概念。

因而,核心成員Andrew Clark開始了曠日持久的優先級算法改造,見:PR lanes

Offscreen支持

在此同時,另外一個成員Luna Ruan在開發一個新API —— Offscreen

能夠理解這是React版的Keep-Alive特性。

訂閱外部源

未開啓CM前,在一次更新以下三個生命週期只會調用一次:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

可是開啓CM後,因爲render階段可能被打斷、重複,因此他們可能被調用屢次。

在訂閱外部源(好比註冊事件回調)時,可能更新不及時或者內存泄漏。

舉個例子:bindEvent是一個基於發佈訂閱的外部依賴(好比一個原生DOM事件):

class App {
  componentWillMount() {
    bindEvent('eventA', data => {
      thie.setState({data});
    });
  }
  componentWillUnmount() {
    bindEvent('eventA');
  }
  render() {
    return <Card data={this.state.data}/>;
  }
}

componentWillMount中綁定,在componentWillUnmount中解綁。

當接收到事件後,更新data

render階段反覆中斷、暫停後,有可能出現:

事件最終綁定前( bindEvent執行前),事件源觸發了事件

此時App組件還未註冊該事件(bindEvent還未執行),那麼App獲取的data就是舊的。

爲了解決這個潛在問題,核心成員Brian Vaughn開發了特性:create-subscription

用來在React中規範外部源的訂閱與更新。

簡單說就是將外部源的註冊與更新在commit階段與組件的狀態更新機制綁定上。

特性層面

源碼層面的支持完備後,基於CM的新特性開發便提上日程。

這即是Suspense

[[Umbrella] Releasing Suspense #13206](https://github.com/facebook/r...PR負責記錄Suspense特性的進展。

Umbrella標記表明這個 PR會影響很是多庫、組件、工具

能夠看到,長長的時間線從18年一直到最近幾天。

最初Suspense只是前端特性,當時React SSR只能向前端傳遞字符串數據(也就是俗稱的脫水

後來React實現了一套SSR時的組件流式傳輸協議,能夠流式傳輸組件,而不只僅是HTML字符串。

此時,Suspense被賦予更多職責。也擁有了更復雜的優先級,這也是剛纔講過的優先級算法改造的一大緣由。

最終的成果,就是今年早些時候推出的Server Components概念。

生態層面

源碼層面支持了、特性也開發完成了,是否是就能無縫接入呢?

還早。

做爲一艘行駛了8年的巨輪,React每次升級到最終社區普及,中間都有巨量的工做要作。

爲了幫助社區慢慢過渡到CMReact作了以下工做:

  • 開發ScrictMode特性,而且是默認啓用的,規範開發者寫法
  • componentWillXXX標記爲unsafe,提醒用戶不要使用,將來會廢棄
  • 提出了新生命週期(getDerivedStateFromPropsgetSnapshotBeforeUpdate)替代如上將被廢棄的生命週期
  • 開發了legacy模式與CM過渡的中間模式 —— blocking模式

而這,只是過渡過程當中最簡單的部分。

難的部分是:

社區當前積累的大量基於 legacy模式的庫如何遷移?

不少動畫庫、狀態管理庫(好比mobX)的遷移並不簡單。

總結

咱們介紹了CM的前因後果以及他遷移的難點。

經過這篇文章,想必你也知道了開頭那個爲React增長createRoot(開啓CM的方法)是多麼不容易。

好在一切都是值得的,若是說之前React的壁壘在於:開源時間早、社區規模大。

那麼從CM開始,React 可能會是前端領域最複雜的視圖框架。

屆時,不會有任何一個React-like的框架能實現React一樣的feature

可是也有人說,CM帶來的這些功能就是雞肋,我根本不須要。

你以爲CM怎麼樣?歡迎留下你的討論。

相關文章
相關標籤/搜索