大話React18,從Fiber到Concurrent Mode

寫在前面

距離上次有存在感的React更新(v16.8)已經(jīng)過去三年多了,那個時候React Hooks也成為了風(fēng)靡一時的知識,直到現(xiàn)在,幾乎以及完全替代了傳統(tǒng)的類組件,期間React又更新了v17版本,但17版本對于開發(fā)者來說,基本也是存在感很低,因為對現(xiàn)有的開發(fā)沒有任何影響,基本上是底層的更新。直到今年的 3 月 29 日,React18迎來了更新,這次更新可謂是“十年磨一劍”,甚至說,之前所有的版本更新,都是為了React18做準(zhǔn)備。那么接下來,就跟著我的腳步一起了解React18吧!

React發(fā)展史

這里為了照顧新手讀者,我們需要介紹一下React的歷史,讓一些來的不是那么突兀;如果你是老手React開發(fā),那么你可以繞過這一段落。
React作為一個優(yōu)秀的前端框架,有著很復(fù)雜的發(fā)展過程,這里我們簡述幾個對開發(fā)者比較有感知的歷史
?? V15,作為一個比較經(jīng)典古老的版本,也為很多React開發(fā)者打下了基礎(chǔ),在V15版本中React組件創(chuàng)建方式是這樣的:

  1. React.createClass(已廢棄)
const App = React.createClass({
  render() {
    return <div>hello</div>
  }
})
  1. Class 組件(類組件、有狀態(tài)組件)
class App extends React.component{
  render() {
    return <div>hello</div>
  }
}
  1. 函數(shù)組件(UI組件、傻瓜組件、無狀態(tài)組件)
const App = () => {
  return <div>hello</>
}

另外,V15中底層更新采用的傳統(tǒng)堆棧diff算法。
?? V16, V16作為一個重要版本,算是最有存在感的一個版本,有以下幾個開發(fā)者感觸比較深的點;

  1. 加入了 React.Fragment Api,減少代碼多余DOM
const App = () => {
  return (
    <React.Fragment> // 不會被渲染
        <div>hello</>
        <div>hello</>
    </React.Fragment>
  )
}
  1. 加入了memo Api,讓函數(shù)組件也擁有了shouldComponentUpdate的能力
const App = (props) => {
  return <div {...props}>hello</>
}
export default React.memo(App)
  1. 加入hooks概念,讓函數(shù)組件擁有了像類組件那樣的能力并通過鉤子的形式讓函數(shù)組件具備各種能力擴展;
const App = (props) => {
  useEffect(() => {
    console.log("更新/掛載了")
  },[])
  return <div {...props}>hello</>
}
export default React.memo(App)

因為hooks內(nèi)容比較多,且不在本文重點討論范圍內(nèi),這里不做贅述,需要了解更多,請移步我的歷史文章《React Hooks,徹底顛覆React,它的未來應(yīng)該是這樣的》

  1. 廢棄了一些生命周期,如:componentWillMount、componentWillUpdate等,改用靜態(tài)方法
  2. 雖然對用戶無太大感知,但這里不得不提一下React Fiber。他將React推向了一個新的高度,取代了原先的堆棧式diff算法。也為后來的React 18以及后續(xù)發(fā)展奠定了基礎(chǔ),這里依然不多說,畢竟React Fiber要想講清楚也是需要一個專門的話題,但簡單總結(jié)一下 Fiber,那就是可中斷的更新機制。我們用下面一個鏈接可以自行對比:
    https://claudiopro.github.io/react-fiber-vs-stack-demo/
    圖中的數(shù)學(xué)模型叫“謝爾并斯基三角”,其特別就是節(jié)點無限增加,如果是傳統(tǒng)的diff算法,那么在復(fù)雜的節(jié)點更新下,會出現(xiàn)肉眼可見的卡頓(頻率小于60HZ)。但在Fiber 算法下,每一個節(jié)點便是一個 Fiber節(jié)點,其更新是可中斷的。所以,看起來很柔和(刷新頻率大于60HZ)。

?? V17 React 可以說是作為一個過渡版本,大部分更新功能對于開發(fā)者沒有感知,不過這里需要說一下比較重要的點,那就是事件代理機制更新了,我們都知道,React是合成事件,其中React17對事件機制做了調(diào)整,下面一幅圖比較清楚


也就是說,在新版本的React中,事件不是冒泡到document中了,而是冒泡到我們的root根節(jié)點下。
另外,就是在React 17中試運行了 Concurrent Mode(并發(fā)模式),這里我們會在React18中詳細介紹;在React18中,Concurrent Mode(并發(fā)模式)成了正式功能,這就是說,為什么React17是一個過渡版本;

React18功能一覽

Concurrent Mode(并發(fā)模式)

正如上面提到的那樣,并發(fā)模式在React17中已經(jīng)被試用了,但直到React18才正式使用,下面我們簡單來說一下;

CM 本身并不是一個功能,而是一個底層設(shè)計,它使 React 能夠同時準(zhǔn)備多個版本的 UI。

所以,他對于現(xiàn)有的功能以及生態(tài)不會有任何影響。

在以前,React 在狀態(tài)變更后,會開始準(zhǔn)備虛擬 DOM,然后渲染真實 DOM,整個流程是串行的。一旦開始觸發(fā)更新,只能等流程完全結(jié)束,期間是無法中斷的。



在 CM 模式下,React 在執(zhí)行過程中,每執(zhí)行一個 Fiber,都會看看有沒有更高優(yōu)先級的更新,如果有,則當(dāng)前低優(yōu)先級的的更新會被暫停,待高優(yōu)先級任務(wù)執(zhí)行完之后,再繼續(xù)執(zhí)行或重新執(zhí)行

這里舉個例子:有一天你上班正在劃水,你打開了一個電影,這時候領(lǐng)導(dǎo)正朝你工位走來,在React18之前,你雖然心里很慌,但也只能等電影播放完才能打開你的編輯器繼續(xù)工作,然后被領(lǐng)導(dǎo)一頓罵,但是在React18CM模式下,當(dāng)你看到領(lǐng)導(dǎo)朝你的工位走來時,你意識到這是個緊急情況,于是你把電影關(guān)掉,打開個編輯器,躲過了領(lǐng)導(dǎo)。

不過對于普通開發(fā)者來說,我們一般是不會感知到 CM 的存在的,在升級到 React 18 之后,我們的項目不會有任何變化。

但我們可以關(guān)注,基于CM實現(xiàn)的功能,這也是未來React18會一直深耕的東西

startTransition

上面提到CM對普通開發(fā)者,沒有感知,但也有一些主動發(fā)揮其優(yōu)勢的案例,下面我們來說一下startTransition
React 的狀態(tài)更新可以分為兩類:

  • 緊急更新(Urgent updates):比如打字、點擊、拖動等,需要立即響應(yīng)的行為,如果不立即響應(yīng)會給人很卡,或者出問題了的感覺
  • 過渡更新(Transition updates):將 UI 從一個視圖過渡到另一個視圖。不需要即時響應(yīng),有些延遲是可以接受的。

CM 只是提供了可中斷的能力,默認情況下,所有的更新都是緊急更新。
因為React并不知道哪些是優(yōu)先級更高的更新??聪旅娴拇ademo

const [inputValue, setInputValue] = useState();
const [searchQuery, setSearchQuery] = useState();

const onChange = (e)=>{
  setInputValue(e.target.value);
  // 更新搜索列表
  setSearchQuery(e.target.value);
}

return (
  <input value={inputValue} onChange={onChange} />
)

上面代碼中,是通過在輸入框中輸入值,來改變searchQuery的值,并且注意到Input還是一個受控組件,也就是說,要有及時的狀態(tài)響應(yīng)。我們根據(jù)上面的定義不難分析出,值在Input組件中的及時反顯是緊急的更新,而參數(shù)的更新是非緊急了,否則在極端情況下就會卡頓。
但是 React 確實沒有能力自動識別。所以它提供了 startTransition讓我們手動指定哪些更新是緊急的,哪些是非緊急的。
所以,在React18中,我們可以這樣改造我們的代碼

const [inputValue, setInputValue] = useState();
const [searchQuery, setSearchQuery] = useState();

const onChange = (e)=>{
  setInputValue(e.target.value);
  // 更新搜索列表
  startTransition(() => { // 指定setSearchQuery為非緊急更新
     setSearchQuery(e.target.value);
  })
}

return (
  <input value={inputValue} onChange={onChange} />
)

下面我們用一個具體的Demo來演示上面講到的


我們要操作一個畢達哥拉斯樹,并且通過上面的<Slider/>組件來控制樹的傾斜,這里我們先簡單解釋一下畢達哥拉斯樹,其原始數(shù)學(xué)模型就像下面這張圖

隨著其對應(yīng)角度的不同以及其層級的數(shù)量不同,樹的傾斜程度和復(fù)雜度也不一樣。因其復(fù)雜的特性,從頁面渲染的角度來講,他可以模擬比較極端的渲染場景。
我們還回到上面的傾斜度控制的demo中,其代碼大概是這樣的

const [treeLean, setTreeLean] = useState(0)

function changeTreeLean(event) {
  const value = Number(event.target.value);
  setTreeLean(value)
}

return (
  <>
    <input type="range" value={treeLean} onChange={changeTreeLean} />
    <Pythagoras lean={treeLean} />
  </>
)

在每次 Slider 拖動后,React 執(zhí)行流程大致如下:

  • 更新 treeLean
  • 渲染 input,填充新的 value
  • 重新渲染樹組件 Pythagoras

但當(dāng)樹的節(jié)點足夠多的時候,Pythagoras 渲染一次就非常慢,就會導(dǎo)致 Slider 的 value 回填變慢,用戶感覺到嚴(yán)重的卡頓。如下圖:



在 React 18 以前,我們是沒有什么好的辦法來解決這個問題的。但是上面我們提到,React18可以通過startTransition來區(qū)分緊急更新,在我們看來,表單的快速回填才是最緊急的,因為這里直接和用戶的動作交互,相應(yīng)不及時,就會有卡頓的現(xiàn)象,基于 React 18 CM 的可中斷渲染機制,我們可以將樹的更新渲染標(biāo)記為低優(yōu)先級的,就不會感覺到卡頓了。
我們這樣改造代碼

const [treeLeanInput, setTreeLeanInput] = useState(0);
const [treeLean, setTreeLean] = useState(0);

function changeTreeLean(event) {
  const value = Number(event.target.value);
  setTreeLeanInput(value)

  // 將 treeLean 的更新用 startTransition 包裹,代表非緊急更新
  React.startTransition(() => {
    setTreeLean(value);
  });
}

return (
  <>
    <input type="range" value={treeLeanInput} onChange={changeTreeLean} />
    <Pythagoras lean={treeLean} />
  </>
)

此時更新流程變?yōu)榱?br> input 更新

  • treeLeanInput 狀態(tài)變更
  • 準(zhǔn)備新的 DOM
  • 渲染 DOM

樹更新(這一次更新是低優(yōu)先級的,隨時可以被中止)

  • treeLean 狀態(tài)變更
  • 準(zhǔn)備新的 DOM
  • 渲染 DOM

React 會在高優(yōu)先級更新渲染完成之后,才會啟動低優(yōu)先級更新渲染,并且低優(yōu)先級渲染隨時可被其它高優(yōu)先級更新中斷。
雖然我們降低了UI渲染的緊急性,但畢竟UI就變得響應(yīng)不那么及時了,React 18 提供了 useTransition 來跟蹤 transition 狀態(tài)。我們可以設(shè)置一個loading來緩解這種UI上的加載卡頓,于是我們可以再次改造我們的代碼


const [treeLeanInput, setTreeLeanInput] = useState(0);
const [treeLean, setTreeLean] = useState(0);

// 實時監(jiān)聽 transition 狀態(tài)
const [isPending, startTransition] = useTransition();

function changeTreeLean(event) {
  const value = Number(event.target.value);
  setTreeLeanInput(value)

  React.startTransition(() => {
    setTreeLean(value);
  });
}

return (
  <>
    <input type="range" value={treeLeanInput} onChange={changeTreeLean} />
    <Spin spinning={isPending}>
      <Pythagoras lean={treeLean} />
    </Spin>
  </>
)

自動批處理 Automatic Batching

批處理是指 React 將多個狀態(tài)更新,聚合到一次 render 中執(zhí)行,以提升性能。比如

function handleClick() {
  setCount(10);
  setFlag(false);
  // React 只會 re-render 一次,這就是批處理
}

在 React 18 之前,React 只會在事件回調(diào)中使用批處理,而在 Promise、setTimeout、原生事件等場景下,是不能使用批處理的。

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 會 render 兩次,每次 state 變化更新一次
}, 1000);

而在 React 18 中,所有的狀態(tài)更新,都會自動使用批處理,不關(guān)心場景。

function handleClick() { // 在回調(diào)事件中
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只會 re-render 一次,這就是批處理
}

setTimeout(() => { // 在setTimeOut中
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只會 re-render 一次,這就是批處理
}, 1000);

如果你在某種場景下不想使用批處理,你可以通過 flushSync來強制同步執(zhí)行(比如:你需要在狀態(tài)更新后,立刻讀取新 DOM 上的數(shù)據(jù)等。)

import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React 更新一次 DOM
  flushSync(() => {
    setFlag(f => !f);
  });
  // React 更新一次 DOM
}

OffScreen

該功能還未正式發(fā)布,不過可以簡單描述下就是

“OffScreen 支持只保存組件的狀態(tài),而刪除組件的 UI 部分?!?/p>

React開發(fā)者再也不用被說“React沒有keep-alive了”當(dāng)然OffScreen 不止是只有實現(xiàn)keep-alive這么簡單

在 OffScreen 中,React 會保存住最后的狀態(tài),下次會用這些狀態(tài)重新渲染組件。

看下面代碼,

async function handleSubmit() {
  setPending(true)
  await post('/someapi')
  setPending(false)
}

在React18以前,如果在請求的過程中組件卸載了,那么就會報出一下錯誤

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

也就是說,你setPending(false)的時候,組件已經(jīng)沒有了,這就造成了內(nèi)存泄漏。
而在React18,OffScreen中,就不會有這樣的問題,因為如果你及時是在請求過程中卸載了組件,那組件的pengding狀態(tài)依然是true
當(dāng)然,這個功能目前還沒有被發(fā)布,等正式發(fā)布了,我們再親自實驗這個問題吧!

新的hooks

  • useDeferredValue

用法:

const deferredValue = useDeferredValue(value);

useDeferredValue 可以讓一個state 延遲生效,只有當(dāng)前沒有緊急更新時,該值才會變?yōu)樽钚轮怠?code>useDeferredValue 和 startTransition 一樣,都是標(biāo)記了一次非緊急更新。

之前 startTransition 的例子,就可以用 useDeferredValue來實現(xiàn)。

const [treeLeanInput, setTreeLeanInput] = useState(0);

const deferredValue = useDeferredValue(treeLeanInput);

function changeTreeLean(event) {
  const value = Number(event.target.value);
  setTreeLeanInput(value)
}

return (
  <>
    <input type="range" value={treeLeanInput} onChange={changeTreeLean} />
    <Pythagoras lean={deferredValue} />
  </>
)

寫在后面

本文我們從React的歷史到React18,在React18中主要圍繞Concurrent Mode(并發(fā)模式)來進行了大量的講解與實驗。實際上,Concurrent Mode是React18中最核心的功能,在未來React會依賴此設(shè)計衍生出更多的功能,當(dāng)然React18這次的更新不只有這些,但這里我們只介紹這么多
退一步講,及時是React18更新了這么重要的東西,但對于普通開發(fā)者來說,我們可能并沒有太多機會接觸到,所以,也不要有太大的學(xué)習(xí)壓力哦,總體來說,React18是為React未來打造的,對于現(xiàn)在的我們來說,他更新的功能可以說是無形的不過也希望讀者朋友們能從本文中了解到關(guān)于React18的新知識

項目源碼

這里我將本文中用到的演示demo代碼放到了個人git倉庫中,大家自取
https://github.com/sorryljt/react18-demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容