小推理:React18比老版React更優秀的一個地方

卡頌發表於2022-03-17

大家好,我卡頌。

React18已經進入RC(release candidate)階段,距離正式版只有一步之遙了。

v18新增了很多特性,今天,我們不聊新特性,而是來講講v18相比老版更優秀的一個細節:

v18中,元件render的次數可能更少

歡迎加入人類高質量前端框架群,帶飛

狀態從何而來

在如下元件中:

function App() {
  const [num, update] = useState(0);
  // ...省略
}

App元件render後會執行useState,返回num的最新值。

也就是說,元件必須render,才能知道最新的狀態。為什麼會這樣呢?

考慮如下觸發更新的程式碼:

const [num, update] = useState(0);
const onClick = () => {
  update(100);
  update(num => num + 1);
  update(num => num * 3);
}

onClick執行後觸發更新,更新導致App元件render,進而useState執行。

useState內部,會遵循如下流程計算num

  1. update(100)num變為100
  2. update(num => num + 1)num變為100 + 1 = 101
  3. update(num => num * 3)num變為101 * 3 = 303

即,App元件render時,num為303。

所以,狀態的計算需要先收集觸發的更新,再在useState中統一計算。

對於上述例子,將更新分別命名為u0~u2,則狀態的計算公式為:

baseState -> u0 -> u1 -> u2 = newState

Concurrent帶來的變化

Concurrent(併發)為React帶來了優先順序的概念,反映到狀態計算上,根據觸發更新的場景,更新擁有不同優先順序(比如onClick回撥中觸發的更新優先順序高於useEffect回撥中觸發的更新)。

表現在計算狀態中的區別就是,如果某個更新優先順序低,則會被跳過。

假設上述例子中u1優先順序低,那麼App元件render時,計算num狀態的公式為:

// 其中u1因為優先順序低,被跳過
baseState -> u0 -> u2 = newState

即:

  1. update(100)num變為100
  2. update(num => num * 3)num變為100 * 3 = 300

顯然這個結果是不對的。

所以,併發情況下React計算狀態的邏輯會更復雜。具體來講,可能包含多輪計算。

當計算狀態時,如果某次更新被跳過,則下次計算時會從被跳過的更新繼續往後計算。

比如上例中,u1被跳過。當u1被跳過時,num為100。此時的狀態100,以及u1他後面的所有更新都會儲存下來,參與下次計算。

在例子中即為u1u2儲存下來。

下次更新的情況如下:

  1. 初始狀態為100update(num => num + 1)num變為100 + 1 = 101
  2. update(num => num * 3)num變為101 * 3 = 303

可見,最終的結果303與同步的React是一致的,只是需要render兩次。

同步的React render一次,結果為303。

併發的React render兩次,結果分別為300(中間狀態),303(最終狀態)。

新舊Concurrent的區別

從上例我們發現,元件render的次數受有多少更新被跳過影響,實際可能不止render兩次,而是多次。

在老版併發的React中,表示優先順序的是一個被稱為expirationTime的時間戳。比較更新是否應該被跳過的演算法如下:

// 更新優先順序是否小於render的優先順序
if (updateExpirationTime < renderExpirationTime) {
  // ...被跳過
} else {
  // ...不跳過
}

在這種邏輯下,只要優先順序低,就會被跳過,就意味著多一次render

在新版併發的React中,優先順序被儲存在31位的二進位制數中。

舉個例子:

const renderLanes = 0b0101;
u1.lane =           0b0001;
u2.lane =           0b0010;

其中renderLanes是本次更新指定的優先順序

比較優先順序的函式為:

function isSubsetOfLanes(set, subset) {
  return (set & subset) === subset;
}

其中:

// true
isSubsetOfLanes(renderLanes, u1.lane)

// false
isSubsetOfLanes(renderLanes, u2.lane)

u1.lane包含於renderLanes中,代表這個更新擁有足夠優先順序。

u2.lane不包含於renderLanes中,代表這個更新沒有足夠優先順序,被跳過。

但是被跳過的更新(例子中的u2)的lane會被重置為0,即:

u2.lane = 0b0000;

顯然任何lanes都是包含0的:

// true
isSubsetOfLanes(renderLanes, 0)

所以這個更新一定會在下次處理。換言之,在新版併發的React中,由於優先順序原因被跳過,導致的重複render,最多隻會有2次。

總結

相比於老版併發的React,新版併發的Reactrender次數上會更有優勢。

反映到使用者的感官上,使用者會更少看到未計算完全的中間狀態

相關文章