大家好,我卡頌
React18
正式版已經發布一段時間了,如果你升級到v18
,且仍使用ReactDOM.render
建立應用,會收到如下報警:
大意是說:v18
使用createRoot
而不是render
建立應用,如果你仍使用render
建立應用,那麼應用的行為將同v17
一樣。
React
團隊之所以有底氣讓大家都升級到v18
,使用createRoot
,是因為他們作出了承諾:
大意是說:如果你升級到v18
,只要不使用併發特性(比如useTransition
),React
會和之前版本表現一致(更新會同步、不可中斷)
今天這篇文章想說的是:某些情況下,上述說法是錯誤的。
歡迎加入人類高質量前端框架群,帶飛
不說廢話,上示例
示例中有a
、b
兩個狀態,首次渲染完2秒後會觸發a
、b
更新。
其中觸發b
更新的方式比較特殊:模擬點選,間接觸發b
更新:
function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const BtnRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
setTimeout(() => {
setA(9000);
BtnRef.current?.click();
}, 2000);
}, []);
return (
<div>
<button
ref={BtnRef}
onClick={() => setB(1)}>
b: {b}
</button>
{Array(a).fill(0).map((_, i) => {
return <div key={i}>{a}</div>;
})}
</div>
);
}
完整示例地址
現在我們有兩種掛載<App/>
的方式。
v18
之前的方式:
const rootElement = document.getElementById("root");
// v18之前建立應用的方式
ReactDOM.render(<App/>, rootElement);
v18
提供的方式:
const root = ReactDOM.createRoot(rootElement);
// v18建立應用的方式
root.render(
<App />
);
為了看清這兩者的區別,有兩種方式:
- 調大
setA(9000)
中的值,使頁面渲染更多項。頁面渲染時卡頓越明顯,渲染順序的差異越明顯
setTimeout(() => {
setA(9000);
BtnRef.current?.click();
}, 2000);
- 在
react-dom.development.js
的commitRootImpl
方法中打斷點
這個方法是React
渲染時呼叫的方法,在這裡打斷點可以看出頁面渲染的順序。
對於ReactDOM.render
建立的應用,觸發更新後渲染順序如下:
首先:
其次:
對於ReactDOM.createRoot
建立的應用,觸發更新後渲染順序如下:
首先:
其次:
渲染順序顯然是變了,這和React
文件裡的說法是相悖的。
背後的原因是什麼呢?
更新的優先順序,無處不在
先解釋下示例中的b
為什麼採用觸發onClick事件的方式間接觸發更新:
BtnRef.current?.click();
這是因為:不同方式觸發的更新有不同優先順序,onClick回撥
中觸發的更新是最高優的,即同步優先順序。
那麼問題來了,v18
不使用併發特性,所有更新不都該是同步、不可中斷麼?
這話是沒錯,更新本身是同步、不可中斷的。但是更新是需要排程的。
在示例中,如果採用ReactDOM.createRoot
建立應用,那麼觸發更新時的優先順序如下:
setTimeout(() => {
// 觸發更新,優先順序為“預設優先順序”
setA(9000);
// 觸發更新,優先順序為“同步優先順序”
BtnRef.current?.click();
}, 2000);
接下來React
的執行流程如下:
a
觸發更新,優先順序為“預設優先順序”- 排程
a
的更新,優先順序為“預設優先順序” b
觸發更新,優先順序為“同步優先順序”- 排程
b
的更新,優先順序為“同步優先順序” - 此時發現已經有個更新在排程(
a
的更新),且優先順序更低(預設優先順序 < 同步優先順序) - 取消
a
的更新的排程,轉而開始排程b
的更新 - 排程流程結束,開始同步、不可中斷的執行
b
的更新 b
對應更新渲染到頁面中- 此時發現還有一個更新(
a
的更新),排程他 - 排程流程結束,開始同步、不可中斷的執行
a
的更新 a
對應更新渲染到頁面中
可見,只要採用ReactDOM.createRoot
建立應用,那麼優先順序的影響就會一直存在,與使用了併發特性的區別是:
- 只有預設優先順序與同步優先順序
- 優先順序只會影響排程,不會中斷更新的執行
老版React的歷史包袱
那麼採用ReactDOM.render
建立的應用執行順序又是怎麼一回事呢?
記不記得一道經典(且毫無意義)的React
面試題:React
的更新是同步還是非同步的?
下面兩種情況,a
列印的結果是1
麼?
// 情況1
onClick() {
this.setState({a: 1});
console.log(a);
}
// 情況2
onClick() {
setTimeout(() => {
this.setState({a: 1});
console.log(a);
})
}
其中,情況2中a
列印結果是1
。
之所以會有這種情況,是React
早期實現批處理
時的瑕疵造成的,並不是什麼有意為之的特性。
當React
使用Fiber
架構重構後,完全可以規避這個瑕疵。但為了與老版本行為保持一致,刻意實現成這樣。
所以,在我們的示例中,這兩個更新不會受到優先順序的影響,但會受到為了相容老版本造成的影響:
setTimeout(() => {
setA(9000);
BtnRef.current?.click();
}, 2000);
React
的執行流程如下:
a
觸發更新,因為是在setTimeout
中觸發的,所以會同步執行後續更新流程a
對應更新渲染到頁面中b
觸發更新,因為是在setTimeout
中觸發的,所以會同步執行後續更新流程b
對應更新渲染到頁面中
總結
React
作為一款維護了快10年的框架,在經歷重大版本更新後要保持框架行為前後一致,實屬不易。
更新順序的變化對一般應用影響不大。
但是,如果你的應用依賴更新後頁面中當前的值作出後續判斷,那麼需要注意升級到v18
後的這些細微變化。