匆匆過客:React17
React17 新增特性:對使用者來說,無新特性, 官方原話:
The React 17 release is unusual because it doesn’t add any new developer-facing features. Instead, this release is primarily focused on making it easier to upgrade React itself.
事件系統重構
作為一個React 庫深度使用者,我個人是特別喜歡這種All In Js的開發方式,整個頁面邏輯都可以用js寫,而不用html與js之間不斷切換、相互設計;
但另一個重要的點就是開發過程不用考慮事件的跨瀏覽器相容問題,因為React已經通過合成事件(SyntheticEvent)做了主流瀏覽器的相容。
雖說使用方式大部分一致,但和原生事件並不完全相同, 差異在於:
- 事件節點的掛載機制,React是採用所有節點事件都掛載在document節點上,採用事件委託與註冊分發的機制實現;
- 事件觸發時機不一致, 雖都遵從向下捕獲向上冒泡,但合成事件會
晚一拍
;具體表現就是,原生事件捕獲 -> 原生事件冒泡 -> react事件捕獲 -> react 事件冒泡; - 回撥事件的入參不一樣,原生的就是事件物件,React的則為合成事件物件,並且他自身有一個事件池專門來管理這些物件;
React 17對事件系統做了重構,其改變點:
- 事件節點的掛載機制改變,從document 變為 root 節點,且這個變化不會造成 root節點之外的Portals 監聽失效;
import React from 'react';
import ReactDOM from 'react-dom';
class Foo extends React.Component {
enter(e){ console.log('click foo');
e.stopPropagation(); }
render() {
return <div style={{ height: 30, background: 'blue' }} onClick={this.enter}>Foo</div>;
}
}
export default class Bar extends React.Component {
enter(e){ console.log('click bar'); }
componentDidMount() {
ReactDOM.render(<Foo />, this.refs.c);
}
render() {
return <div style={{ height: 50, background: 'red', color: 'white' }} onClick={this.enter}>Bar <div ref="c"/></div>;
}
}
看上面的示例,頁面 UI 如下圖,如果是17以前,當我們點選Foo區域時,期望Bar區域不要響應,縱使加了stopPropagation, 也不能阻止;但隨著17的到來,這個bug就可以避免,這也是為漸進式升級
做鋪墊
- 事件回撥時機和原生事件步調一致(這真的是個大提升);
- SyntheticEvent 不再存入事件池,所以e.persit()就不再生效;
承上啟下
v17開啟了React漸進式升級的新篇章(略微有點虛張聲勢)
17以後,允許同一個頁面上使用不同的 React 版本。根據官方的demo示例,是基於懶載入的特性實現,用法有點類似於SPA微應用的玩法。個人覺得這並沒有黑魔法,如果不用二次載入,而期望某個元件用單獨的react版本,這該報錯還是報錯,就像下面這樣:
所以通過了解前面兩個特性,確實是印證官方那句話:17只是為了給18及以後的版本鋪路,為了讓使用者更簡單的去升級版本
React18 新特性
自動批處理(Auto Bacthing)
批處理是 React 將多個狀態更新分組到一個重新渲染中,以獲得更好的效能(減少render次數)。
在React 18以前,批處理更新只會發生在React事件回撥中,而在Promise、setTimeOut、原生事件回撥或或任何其他事件內部的更新都沒有采用批處理,舉個?:
// 18以前: .
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// render 2次
}, 1000);
// 18及以後:
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// render1次
}, 1000);
繞過黑魔法:flushSync
自定義渲染優先順序
在react16退出非同步渲染時,提出了渲染優先順序的概念,like: 使用者輸入 > 動畫 > 列表展示。在多個渲染任務到來時,它會優先渲染高優先順序的任務。
但由於這是一個理想的設想,程式自己是很難判斷什麼一定是高優先順序,因為使用者使用場景太複雜了;所以在18中,這個底層能力進一步開放,推出了幾個hooks:startTransition、useTransition、 useDeferredValue等,這些hooks 可以讓使用者自定義渲染優先順序
舉個例子,輸入搜尋框,直觀點:
下面是虛擬碼,直接看demo演示直接一點:
import {startTransition} from 'react';
// 高優先順序: 輸入資料回顯
setInputValue(input);
// 使用Transition 對低優先順序的渲染進行標記
startTransition(() => {
setQuery(input);
});
官方的列子,我覺得對圖形化的使用者會更易感觸,有興趣的可以瞭解一下:<官方示例>
所以最新版的React將 state 的更新分成了兩類:
- 緊急更新(Urgent updates)將直接作用於使用者互動,比如輸入、點選等等
- 過渡更新(Transition updates)將 UI 從一個檢視過渡到另一個檢視
通過這些hooks 我們可以自定義渲染優先順序。
新的入口掛載API:createRoot
// 18 以前:
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);
// 18 以後:
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container); //
root.render(<App tab="home" />);
值得一提的是,18對老的掛載方式也是相容的,只是這種用法沒法使用新特性(開發環境會提示), 這也是漸進式升級的一部分
漸進式升級
官方原話:從技術上講,併發渲染是一個突破性的升級。因為併發渲染是可中斷的,所以啟用它時元件的行為會略有不同,這個比例大概0.2%.
所以官方為了大家更加順滑的升級到18,提出了漸進式升級
,提供一個StrictMode API,使用這個模組包裹的節點,將遵從嚴格模式,這有助於:
- 識別不安全的生命週期
- 使用過時字串 ref API 的警告
- 使用廢棄的 findDOMNode 方法的警告
- 檢測意外的副作用
- 檢測過時的 context API
- 確保可複用的狀態
下面的demo, Header 和 Footer 將以正常模式渲染,而被StrictMode包裹的Component節點,將以嚴格模式渲染
import React from 'react';
function ExampleApplication() {
return (
<div>
<Header />
<React.StrictMode>
<div>
<ComponentOne />
<ComponentTwo />
</div>
</React.StrictMode>
<Footer />
</div>
);
}
嚴格模式不能自動檢測到你應用版本升級帶來的副作用,僅可以幫助發現它們,使它們更具確定性,通過故意重複呼叫一些宣告函式來實現(具體請檢視官方文件)
// 正常模式
* React mounts the component.
* Layout effects are created.
* Effects are created.
// 嚴格模式
* React mounts the component.
* Layout effects are created.
* Effect effects are created.
* React simulates effects being destroyed on a mounted component.
* Layout effects are destroyed.
* Effects are destroyed.
* React simulates effects being re-created on a mounted component.
* Layout effects are created
* Effect setup code runs
雖說這個模組只在開發環境有效,但細心的人會發現,重複的銷燬和掛載他還是會帶來新的問題,例如:
const [value, setValue] = useState(0);
useEffect(() => {
setValue(val => val + 1);
})
// 正常模式列印的是1,但嚴格模式這會列印2;
關於這個問題國內已經有文章開始討論,react 倉庫也有一個 issue是關於這個問題的討論,感興趣的可以戳這裡。
淺析併發渲染
首先在說react渲染之前,複習一個知識點:瀏覽器中的js執行和UI渲染是在一個執行緒中順序發生(包含js執行,事件響應,定時器,UI渲染),且js的執行是單執行緒(基於eventloop)。
這個單執行緒就決定了,處理A就處理不了B。
有了這個共識,我們來理一理react渲染史
同步渲染
最早的react(16之前)是同步的,就是指當使用者通過 setState 觸發一個更新,到更新渲染到頁面,會經歷兩個過程:
- diff階段:會根節點開始diff,找出更新前後dom樹的差異;
- commit階段:根據diff的結果,更新UI到頁面;
由於整個過程是同步的,會一直佔據js執行緒;所以在一個更新的過程中,頁面發生的點選、輸入等互動事件都會等待,直到這次更新完成,顯然這個體驗是糟糕的。
非同步渲染
為了解決同步渲染阻塞主執行緒的問題,那就讓渲染變的更加靈活--造成這些最大的問題不是效能,而是排程(React Conf 2018)。
基於此一個轟動前端圈的名詞誕生-Fiber
,新的渲染架構最大的特點就是將以往一條路走到黑的玩法分成了兩個階段:
- render階段: 一個連結串列結構的任務鏈,是可打斷的;
- commit階段: 就是把UI更新到介面的過程,這是同步的,不可打斷的;
通過連結串列表示render階段節點之間的關係,並加持時間切片的方式,連結串列上節點是否繼續向後比對,取決於當前執行緒是否空閒; 並且react會將每個更新任務標註優先順序,如果新的更新優先順序高於正在處理的任務,那麼前一次任務就會被打斷廢棄,從而處理更高優先順序的任務;
這裡需要明白的是: 一旦一個更新任務被打斷廢棄,高優先順序任務執行完後,這個任務是需要從頭再來一遍的計算的。
另一個要記住的點是,是先有Fiber再有hooks,而不是hooks帶來了Fiber。
併發渲染
只要你讀過React18的一些文章,你可能會聽過Concurrent Mode、Concurrent React 或者 Concurrent features, 這些名詞都不重要,重要的是明白他的目的。
首先,先糾正一下大家聽到非同步渲染可能產生的一個錯誤臆斷:非同步渲染不是指一個樹的兩個或多個分支同時被渲染。
動下腳指頭想一想,這怎麼可能,這本身就是與瀏覽器渲染原理就是相悖的。
官方原話:
Concurrency is not a feature, per se. It’s a new behind-the-scenes mechanism that enables React to prepare multiple versions of your UI at the same time.
multiple versions of your UI
圈起來,考試要考。
併發渲染成功解決了非同步渲染中中斷廢棄的問題,他是中斷可恢復繼續的;不過在一些場景,也會存在中斷廢棄的問題。
這個能力,在以後也能解鎖另一個使用場景:狀態複用。比如一個tab切換,當從a 切到 b,再從b切回a時,這時候通過某些實現,我們就可以狀態複用,快速的在螢幕上展示出a;
併發渲染帶來的意義遠不止這些(還包括對server,native端),我只撿了點我能看懂的分享給大家。反正官方寫的非常美好,我個人還是比較期待。
推薦閱讀:
總結
通過對react18 新特性的熟悉及渲染模式的講解,讓我們可以感受到,這個版本將對我們應用的體驗將帶來肉眼可見的提升,前提是你不濫用,並且會用。
並且隨著基於底層的不斷暴露和併發模式不斷的建設,以後可能我們用的就不是react,而是Remix,Next這種基於react庫建立起來的生態框架,這也是React工作組成立起來的意義之一,更好的打造React生態。
很難想象,這居然是是我2022年第一篇文章。今年全中國最迷茫的是中國經濟,第二可能就是我了,但願這是個轉折!!!