天天看點

React18 有哪些變化?

作者 | 遊鹿
React18 有哪些變化?

可能是React15到16的不相容變更太多,開發者們更新相當痛苦,是以很長一段時間React開發者都沒有再釋出新版本,而是在 v16 上內建各種新能力,16.3/16.8/16.12 幾乎每隔幾個版本就有一顆賽艇的新特性出現。

在長達2年半的 v16 版本後,React團隊釋出了 v17,同時宣布這一版本的定位是一版技術改造的過渡版本,主要目标是降低後續版本的更新成本。在 v17 之前,不同版本的 React 無法混用,很重要的一個原因是之前版本中事件委托是挂在document上的,v17 開始,事件委托挂載到了渲染 React 樹的根 DOM 容器中,這使多 React 版本并存成為了可能。(意味着React 17+可混用,老頁面維持 v17,新頁面使用v18 v19 等)

React18 有哪些變化?

我們越來越能感受到,React的開發者把更新重點放到了「漸進更新」上,僅在v17釋出了2個小版本後,v18的alpha就出現了,并且隻需要使用者做極小、甚至不需要改動就能讓現有React APP在 v18 上工作。那麼v18中有哪些新變化、新特性呢?

注:本文内容來自 reactwg/react-18 的部分discussion,筆者翻譯閱讀了解後寫出來的。如果有了解不到位的地方,歡迎大家評論區交流、讨論、指正~

React 18

React18的更新政策是「漸進更新」,包括名聲在外的并發渲染等在内的新能力都是可選的,不會立刻對元件行為帶來任何明顯的破壞性變化。

You can upgrade to React 18 with minimal or no changes to your application code, with a level of effort comparable to a typical major React release.

你幾乎不需要對應用程式中的代碼進行任何改動就可以直接更新到 React 18,并不會比以往的 React 版本更新要困難。

React官網

https://reactjs.org/blog/2021/06/08/the-plan-for-react-18.html

并發渲染是React底層的一次重要架構設計更新,并發渲染的優勢在于提高React APP性能。當你使用了一些React18新特性後,你可能已經用上了并發渲染。

新的 Root API

在React18中,

ReactDOM.render()

正式成為Legacy,并增加了新的RootAPI

ReactDOM.createRoot()

,他們的用法差别如下:

import ReactDOM from ‘react-dom’;
import App from 'App';

ReactDOM.render(<App />, document.getElementById('root'));           
import ReactDOM from ‘react-dom’;
import App from 'App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(<App />);           

可以看到,通過新的API,我們可以為一個React App建立多個根節點,甚至在未來可以用不同版本的React來建立。React18 保留了上述兩種用法,老項目不想改仍然可以用

ReactDOM.render()

;新項目想提升性能,可以用

ReactDOM.createRoot()

借并發渲染的東風。

自動 Batching

什麼是Batching

Batching is when React groups multiple state updates into a single re-render for better performance.

為了使應用獲得更好的性能,React把多次的狀态更新(state updates),合并到一次渲染中。

React17隻會把浏覽器事件(如點選)發生期間的狀态更新合并掉。而React18會把事件處理器發生後的狀态更新也合并掉。舉個例子:

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClickPrev() {
     setCount(c => c - 1); // Does not re-render yet
    setFlag(f => !f); // Does not re-render yet
    // React will only re-render once at the end (that's batching!)
  }
  
  function handleClickNext() {
    fetchSomething().then(() => {
      // React 17 and earlier does NOT batch these because
      // they run *after* the event in a callback, not *during* it
      setCount(c => c + 1); // Causes a re-render
      setFlag(f => !f); // Causes a re-render
    });
  }

  return (
    <div>
      <button onClick={handleClickPrev}>Prev</button>
      <button onClick={handleClickNext}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}           

什麼是Automatic Batching

在React 18中,隻要使用 新的 Root API

ReactDOM.createRoot()

方法,就能直接享受自動batching的能力!這裡列舉一些自動更新的場景:

React18 有哪些變化?

不使用automatic batching

batching 是安全的,但也存在一些特殊情況不希望batching發生,比如:你需要在狀态更新後,立刻讀取新DOM上的資料等。這種情況下請使用 ReactDOM.flushSync() (React官方不推薦常态化使用這一API):

import { flushSync } from 'react-dom'; // Note: react-dom, not react

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React has updated the DOM by now
  flushSync(() => {
    setFlag(f => !f);
  });
  // React has updated the DOM by now
}           

對Hooks/Classes的影響

  • 對 Hooks 沒有任何影響
  • 對 Classes 大部分情況下沒影響,關注一種模式:是否在兩次setState之間讀取了state值。差異如下:
handleClick = () => {
  setTimeout(() => {
    this.setState(({ count }) => ({ count: count + 1 }));

    // 在 React17 及之前,列印出來是 { count: 1, flag: false }
    // 在 React18,列印出來是 { count: 0, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};           

如果不想通過調整代碼邏輯的方式進行修正,可以直接采用 ReactDOM.flushSync() :

handleClick = () => {
  setTimeout(() => {
    ReactDOM.flushSync(() => {
      this.setState(({ count }) => ({ count: count + 1 }));
    });

    // 在 React18,列印出來是 { count: 1, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};           

新的 Suspense SSR 架構

Suspense在React16/18的差別

Suspense早在React16就以試驗性API的形式出來了,相比較舊版本的Legacy Suspense,新版本的Concurrent Suspense更符合使用者直覺。

React官方說這兩個版本存在比較小的差異,但由于新版 Suspense 的實作是基于并發渲染的,是以這仍然是一個Breaking Changes,這裡介紹下差異:

<Suspense fallback={<Loading />}>
  <ComponentThatSuspends />
  <Sibling />
</Suspense>           
React18 有哪些變化?

現存SSR架構的問題

現存SSR架構原理不多解釋,它的問題在于,一切都是串行的,在任一前序任務沒完成之前,後一任務都無法開始,也就是“All or Nothing”,一般是如下流程:

  1. 伺服器内部擷取資料
  2. 伺服器内部渲染 HTML
  3. 用戶端從遠端加載代碼
  4. 用戶端開始hydrate(水合)

React18新政策

React18提供了Suspense,打破了這種串行的限制,優化前端的加載速度和可互動所需等待時間。這一SSR架構依賴兩個新特性:

  • 伺服器端的「流式HTML」:使用API pipeToNodeWritable
  • 用戶端的「選擇性Hydration」:使用

新版本Suspense SSR速度更快的原理是什麼呢?以下面的結構為例:

1、流式 HTML

<Layout>
  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>           

如上一個頁面結構,<

Comments

> 是通過接口異步擷取的,這一過程資料請求比較慢,是以我們把它包裹在 <

Suspense

> 裡。在普通的SSR架構裡,一般隻能等<

Comments

>加載進來之後才能進行下一環節。在新模式下,HTML流首先傳回的内容裡是不會有 <

Comments

> 元件相關HTML資訊的,取而代之的是 <

Spinnger

> 的HTML:

<main>
  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section id="comments-spinner">
    <!-- Spinner -->
    <img width=400 src="spinner.gif" alt="Loading..." />
  </section>
</main>           

等服務端擷取到了 <

Comments

> 的資料後,React再把後加入的 <

Comments

> 的HTML資訊,通過同一個流(stream)發送過去,React會建立一個超小的内聯 <

script

> 标簽,把 HTML 放在“正确的位置”。

<div hidden id="comments">
  <!-- Comments -->
  <p>First comment</p>
  <p>Second comment</p>
</div>
<script>
  // This implementation is slightly simplified
  document.getElementById('sections-spinner').replaceChildren(
    document.getElementById('comments')
  );
</script>           

是以與傳統的 HTML 流不同,它不必按自上而下的順序發生。

2、選擇性 Hydration

代碼拆分是我們常用的手段,我們可以用

React.lazy

把一部分代碼從主包中拆出來。

import { lazy } from 'react';

const Comments = lazy(() => import('./Comments.js'));

// ...

<Suspense fallback={<Spinner />}>
  <Comments />
</Suspense>           

在React18以前,

React.lazy

不支援服務端渲染,即便是最流行的解決方案也讓大家從「為了代碼拆分不使用SSR」和「使用SSR但要在所有js加載完成後才能hydratie」中二選一。

而在React18版本,被 <

Suspense

> 包裹的子元件可以延後hydratie,這一行為是React内部自動做掉的,是以

React.lazy

也預設支援了SSR。

新特性startTransition

為了解決什麼問題?

使用此 API 可以防止内部函數執行拖慢 UI 響應速度。

以查詢選擇器為例:使用者輸入關鍵詞,請求遠端資料并展示搜尋結果。

// Urgent: Show what was typed
setInputValue(input);

// Not urgent: Show the results
setSearchQuery(input);           

輸入文字時使用者是希望得到即時回報的,而查詢并展示結果則是允許有延遲的(事實上,開發者經常人為地用一些手段讓他們延遲更新,比如debounce)

在引入 startTransition 後用法是:

import { startTransition } from 'react';

// Urgent: Show what was typed
setInputValue(input);

// Mark any state updates inside as transitions
startTransition(() => {
  // Transition: Show the results
  setSearchQuery(input);
});           

什麼是transition?

更新可以分為兩類:

  • 緊急更新(Urgent Updates):比如打字、點選、拖動等,在直覺上需要立即響應的行為,如果不立即響應會給人感覺出錯了;
  • 過渡更新(Transition Updates):将 UI 從一個視圖過渡到另一個視圖。它不需要即時響應,有點延遲是在預期範圍内、可接受的。

其實在React應用中,大部分更新在概念上都應當是Transition Updates,但是出于向後相容的角度考慮,transition是可選的,是以在React18中預設的更新方式仍然是Urgent Updates,想要使用Transition Updates可以把函數用startTransition 包裹起來。

startTransition 與 setTimeout的差別是什麼?

主要有兩點:

  1. startTransition更早于setTimeout處理渲染更新,這一差别在一些性能較差的機器上感覺稍微明顯。在運作時,startTransition與普通函數一樣,都是立即執行的,隻不過函數執行帶來的所有update會被标記為"transition",React在處理更新時會使用這一标記作為參考。
  2. setTimeout内的大型的螢幕更新會鎖定頁面,在此期間使用者無法與頁面互動。而被标記為“transition”的更新是可被打斷的,

何時使用

慢渲染:一些React需要花費大量時間的複雜渲染

慢網絡:耗時的網絡請求

參考文檔

React文檔

https://reactjs.org/blog/2020/10/20/react-v17.html https://reactjs.org/blog/2020/08/10/react-v17-rc.html https://reactjs.org/blog/2020/02/26/react-v16.13.0.html

React工作組讨論

https://github.com/reactwg/react-18/discussions https://github.com/reactwg/react-18/discussions/7 https://github.com/reactwg/react-18/discussions/4 https://github.com/reactwg/react-18/discussions/21 https://github.com/reactwg/react-18/discussions/37

其他

https://zhuanlan.zhihu.com/p/379072979
React18 有哪些變化?

繼續閱讀