大話React18,從Fiber到Concurrent Mode

寫在前面

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

React發展史

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

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

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

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

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

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

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


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

React18功能一覽

Concurrent Mode(并發模式)

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

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

所以,他對于現有的功能以及生態不會有任何影響。

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



在 CM 模式下,React 在執行過程中,每執行一個 Fiber,都會看看有沒有更高優先級的更新,如果有,則當前低優先級的的更新會被暫停,待高優先級任務執行完之后,再繼續執行或重新執行

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

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

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

startTransition

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

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

CM 只是提供了可中斷的能力,默認情況下,所有的更新都是緊急更新。
因為React并不知道哪些是優先級更高的更新。看下面的代碼demo

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還是一個受控組件,也就是說,要有及時的狀態響應。我們根據上面的定義不難分析出,值在Input組件中的及時反顯是緊急的更新,而參數的更新是非緊急了,否則在極端情況下就會卡頓。
但是 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/>組件來控制樹的傾斜,這里我們先簡單解釋一下畢達哥拉斯樹,其原始數學模型就像下面這張圖

隨著其對應角度的不同以及其層級的數量不同,樹的傾斜程度和復雜度也不一樣。因其復雜的特性,從頁面渲染的角度來講,他可以模擬比較極端的渲染場景。
我們還回到上面的傾斜度控制的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 執行流程大致如下:

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

但當樹的節點足夠多的時候,Pythagoras 渲染一次就非常慢,就會導致 Slider 的 value 回填變慢,用戶感覺到嚴重的卡頓。如下圖:



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

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} />
  </>
)

此時更新流程變為了
input 更新

  • treeLeanInput 狀態變更
  • 準備新的 DOM
  • 渲染 DOM

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

  • treeLean 狀態變更
  • 準備新的 DOM
  • 渲染 DOM

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


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

// 實時監聽 transition 狀態
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 將多個狀態更新,聚合到一次 render 中執行,以提升性能。比如

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

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

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

而在 React 18 中,所有的狀態更新,都會自動使用批處理,不關心場景。

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

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

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

import { flushSync } from 'react-dom';

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

OffScreen

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

“OffScreen 支持只保存組件的狀態,而刪除組件的 UI 部分。”

React開發者再也不用被說“React沒有keep-alive了”當然OffScreen 不止是只有實現keep-alive這么簡單

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

看下面代碼,

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)的時候,組件已經沒有了,這就造成了內存泄漏。
而在React18,OffScreen中,就不會有這樣的問題,因為如果你及時是在請求過程中卸載了組件,那組件的pengding狀態依然是true
當然,這個功能目前還沒有被發布,等正式發布了,我們再親自實驗這個問題吧!

新的hooks

  • useDeferredValue

用法:

const deferredValue = useDeferredValue(value);

useDeferredValue 可以讓一個state 延遲生效,只有當前沒有緊急更新時,該值才會變為最新值。useDeferredValuestartTransition 一樣,都是標記了一次非緊急更新。

之前 startTransition 的例子,就可以用 useDeferredValue來實現。

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

項目源碼

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容