「React18新特性」深度解讀之UseMutableSource
一 前言
大家好,我是 ?? ,接下來會(huì)出一個(gè)新系列,React v18新特性解讀,主要針對新特性的產(chǎn)生背景,功能介紹,和原理分析等幾個(gè)方面,勇于做第一個(gè)吃螃蟹的人。希望支持我的朋友可以點(diǎn)贊,轉(zhuǎn)發(fā),再看,關(guān)注一波公眾號(hào),持續(xù)分享前端技術(shù)硬文。
useMutableSource 最早的 RFC 提案在 2020年 2 月份就開始了。在 React 18 中它將作為新特性出現(xiàn)。用一段提案中的描述來概括 useMutableSource。
useMutableSource 能夠讓 React 組件在 Concurrent Mode 模式下安全地有效地讀取外接數(shù)據(jù)源,在組件渲染過程中能夠檢測到變化,并且在數(shù)據(jù)源發(fā)生變化的時(shí)候,能夠調(diào)度更新。
說起外部數(shù)據(jù)源就要從 state 和更新說起 ,無論是 React 還是 Vue 這種傳統(tǒng) UI 框架中,雖然它們都采用虛擬 DOM 方式,但是還是不能夠把更新單元委托到虛擬 DOM 身上來,所以更新的最小粒度還是在組件層面上,由組件統(tǒng)一管理數(shù)據(jù) state,并參與調(diào)度更新。
回到我們的主角 React 上,既然由組件 component 管控著狀態(tài) state。那么在 v17 和之前的版本,React 想要視圖上的更新,那么只能通過更改內(nèi)部數(shù)據(jù) state ??v覽 React 的幾種更新方式,無一離不開自身 state 。先來看一下 React 的幾種更新模式。
- 組件本身改變 state 。函數(shù) useState | useReducer ,類組件 setState | forceUpdate 。
- props 改變,由組件更新帶來的子組件的更新。
- context 更新,并且該組件消費(fèi)了當(dāng)前 context 。
- 無論是上面哪種方式,本質(zhì)上都是 state 的變化。
- props 改變來源于父級組件的 state 變化。
- context 變化來源于 Provider 中 value 變化,而 value 一般情況下也是 state 或者是 state 衍生產(chǎn)物。
從上面可以概括出:state和視圖更新的關(guān)系 Model => View 。但是 state 僅限于組件內(nèi)部的數(shù)據(jù),如果 state 來源于外部(脫離組件層面)。那么如何完成外部數(shù)據(jù)源轉(zhuǎn)換成內(nèi)部狀態(tài), 并且數(shù)據(jù)源變化,組件重新 render 呢?
常規(guī)模式下,先把外部數(shù)據(jù) external Data 通過 selector 選擇器把組件需要的數(shù)據(jù)映射到 state | props 上。這算是完成了一步,接下來還需要 subscribe 訂閱外部數(shù)據(jù)源的變化,如果發(fā)生變化,那么還需要自身去強(qiáng)制更新 forceUpdate 。下面兩幅圖表示數(shù)據(jù)注入和數(shù)據(jù)訂閱更新。
典型的外部數(shù)據(jù)源就是 redux 中的 store ,redux 是如何把 Store 中的 state ,安全的變成組件的 state 的。
或許我可以用一段代碼來表示從 react-redux 中 state 改變到視圖更新的流程。
- const store = createStore(reducer,initState)
- function App({ selector }){
- const [ state , setReduxState ] = React.useState({})
- const contextValue = useMemo(()=>{
- /* 訂閱 store 變化 */
- store.subscribe(()=>{
- /* 用選擇器選擇訂閱 state */
- const value = selector(data.getState())
- /* 如果發(fā)生變化 */
- if(ifHasChange(state,value)){
- setReduxState(value)
- }
- })
- },[ store ])
- return <div>...</div>
- }
但是例子中代碼,沒有實(shí)際意義,也不是源代碼,我這里就是讓大家清晰地了解流程。redux 和 react 本質(zhì)上是這樣工作的。
- 通過 store.subscribe 來訂閱 state 變化,但是本質(zhì)上要比代碼片段中復(fù)雜的多,通過 selector (選擇器)找到組件需要的 state。我在這里先解釋一下selector,因?yàn)樵跇I(yè)務(wù)組件往往不需要整個(gè) store 中的 state 全部數(shù)據(jù),而是僅僅需要下面的部分狀態(tài),這個(gè)時(shí)候就需要從 state 中選擇‘有用的’,并且和 props 合并,細(xì)心的同學(xué)應(yīng)該發(fā)現(xiàn),選擇器需要和 react-redux 中 connect 第一參數(shù) mapStateToProps 聯(lián)動(dòng)。對于細(xì)節(jié),無關(guān)緊要,因?yàn)榻裉熘攸c(diǎn)是 useMutableSource。
如上是沒有 useMutableSource 的情況,現(xiàn)在用 useMutableSource 不在需要把訂閱到更新流程交給組件處理。如下:
- /* 創(chuàng)建 store */
- const store = createStore(reducer,initState)
- /* 創(chuàng)建外部數(shù)據(jù)源 */
- const externalDataSource = createMutableSource( store ,store.getState() )
- /* 訂閱更新 */
- const subscribe = (store, callback) => store.subscribe(callback);
- function App({ selector }){
- /* 訂閱的 state 發(fā)生變化,那么組件會(huì)更新 */
- const state = useMutableSource(externalDataSource,selector,subscribe)
- }
通過 createMutableSource 創(chuàng)建外部數(shù)據(jù)源,通過 useMutableSource 來使用外部數(shù)據(jù)源。外部數(shù)據(jù)源變化,組件自動(dòng)渲染。
如上是通過 useMutableSource 實(shí)現(xiàn)的訂閱更新,這樣減少了 APP 內(nèi)部組件代碼,代碼健壯性提升,一定程度上也降低了耦合。接下來讓我們?nèi)矫嬲J(rèn)識(shí)一下這個(gè) V18 的新特性。
二 功能介紹
具體功能介紹流程還是參考最新的 RFC, createMutableSource 和 useMutableSource 在一定的程度上,有點(diǎn)像 createContext 和 useContext ,見名知意,就是創(chuàng)建與使用。不同的是 context 需要 Provider 去注入內(nèi)部狀態(tài),而今天的主角是注入外部狀態(tài)。那么首先應(yīng)該看一下兩者如何使用。
創(chuàng)建
createMutableSource 創(chuàng)建一個(gè)數(shù)據(jù)源。它有兩個(gè)參數(shù):
- const externalDataSource = createMutableSource( store ,store.getState() )
第一個(gè)參數(shù):就是外部的數(shù)據(jù)源,比如 redux 中的 store,
第二個(gè)參數(shù):一個(gè)函數(shù),函數(shù)的返回值作為數(shù)據(jù)源的版本號(hào),這里需要注意??的是,要保持?jǐn)?shù)據(jù)源和數(shù)據(jù)版本號(hào)的一致性,就是數(shù)據(jù)源變化了,那么數(shù)據(jù)版本號(hào)就要變化,一定程度上遵循 immutable 原則(不可變性)??梢岳斫鉃閿?shù)據(jù)版本號(hào)是證明數(shù)據(jù)源唯一性的標(biāo)示。
api介紹
useMutableSource 可以使用非傳統(tǒng)的數(shù)據(jù)源。它的功能和 Context API 還有 useSubscription 類似。(沒有使用過 useSubscription 的同學(xué),可以了解一下 )。
先來看一下 useMutableSource 的基本使用:
- const value = useMutableSource(source,getSnapShot,subscribe)
useMutableSource 是一個(gè) hooks ,它有三個(gè)參數(shù):
- source:MutableSource < Source > 可以理解為帶記憶的數(shù)據(jù)源對象。
- getSnapshot:( source : Source ) => Snapshot :一個(gè)函數(shù),數(shù)據(jù)源作為函數(shù)的參數(shù),獲取快照信息,可以理解為 selector ,把外部的數(shù)據(jù)源的數(shù)據(jù)過濾,找出想要的數(shù)據(jù)源。
- subscribe: (source: Source, callback: () => void) => () => void:訂閱函數(shù),有兩個(gè)參數(shù),Source 可以理解為 useMutableSource 第一個(gè)參數(shù),callback 可以理解為 useMutableSource 第二個(gè)參數(shù),當(dāng)數(shù)據(jù)源變化的時(shí)候,執(zhí)行快照,獲取新的數(shù)據(jù)。
useMutableSource 特點(diǎn)
useMutableSource 和 useSubscription 功能類似:
- 兩者都需要帶有記憶化的‘配置化對象’,從而從外部取值。
- 兩者都需要一種訂閱和取消訂閱源的方法 subscribe。
除此之外 useMutableSource 還有一些特點(diǎn):
- useMutableSource 需要源作為顯式參數(shù)。也就是需要把數(shù)據(jù)源對象作為第一個(gè)參數(shù)傳入。
- useMutableSource 用 getSnapshot 讀取的數(shù)據(jù),是不可變的。
關(guān)于 MutableSource 版本號(hào)
- useMutableSource 會(huì)追蹤 MutableSource 的版本號(hào),然后讀取數(shù)據(jù),所以如果兩者不一致,可能會(huì)造成讀取異常的情況。useMutableSource 會(huì)檢查版本號(hào):
- 在第一次組件掛載的時(shí)候,讀取版本號(hào)。
- 在組件 rerender 的時(shí)候,確保版本號(hào)一致,然后在讀取數(shù)據(jù)。不然會(huì)造成錯(cuò)誤發(fā)生。
確保數(shù)據(jù)源和版本號(hào)的一致性。
設(shè)計(jì)規(guī)范
當(dāng)通過 getSnapshot 讀取外部數(shù)據(jù)源的時(shí)候,返回的 value 應(yīng)該是不可變的。
- 正確寫法:getSnapshot: source => Array.from(source.friendIDs)
- 錯(cuò)誤寫法:getSnapshot: source => source.friendIDs
數(shù)據(jù)源必須有一個(gè)全局的版本號(hào),這個(gè)版本號(hào)代表整個(gè)數(shù)據(jù)源:
- 正確寫法:getVersion: () => source.version
- 錯(cuò)誤寫法:getVersion: () => source.user.version
接下來參考 github 上的例子,我講一下具體怎么使用:
例子一
例子一:訂閱 history 模式下路由變化
比如有一個(gè)場景就是在非人為情況下,訂閱路由變化,展示對應(yīng)的 location.pathname,看一下是如何使用 useMutableSource 處理的。在這種場景下,外部數(shù)據(jù)源就是 location 信息。
- // 通過 createMutableSource 創(chuàng)建一個(gè)外部數(shù)據(jù)源。
- // 數(shù)據(jù)源對象為 window。
- // 用 location.href 作為數(shù)據(jù)源的版本號(hào),href 發(fā)生變化,那么說明數(shù)據(jù)源發(fā)生變化。
- const locationSource = createMutableSource(
- window,
- () => window.location.href
- );
- // 獲取快照信息,這里獲取的是 location.pathname 字段,這個(gè)是可以復(fù)用的,當(dāng)路由發(fā)生變化的時(shí)候,那么會(huì)調(diào)用快照函數(shù),來形成新的快照信息。
- const getSnapshot = window => window.location.pathname
- // 訂閱函數(shù)。
- const subscribe = (window, callback) => {
- //通過 popstate 監(jiān)聽 history 模式下的路由變化,路由變化的時(shí)候,執(zhí)行快照函數(shù),得到新的快照信息。
- window.addEventListener("popstate", callback);
- //取消監(jiān)聽
- return () => window.removeEventListener("popstate", callback);
- };
- function Example() {
- // 通過 useMutableSource,把數(shù)據(jù)源對象,快照函數(shù),訂閱函數(shù)傳入,形成 pathName。
- const pathName = useMutableSource(locationSource, getSnapshot, subscribe);
- // ...
- }
來描繪一下流程:
- 首先通過 createMutableSource 創(chuàng)建一個(gè)數(shù)據(jù)源對象,該數(shù)據(jù)源對象為 window。用 location.href 作為數(shù)據(jù)源的版本號(hào),href 發(fā)生變化,那么說明數(shù)據(jù)源發(fā)生變化。
- 獲取快照信息,這里獲取的是 location.pathname 字段,這個(gè)是可以復(fù)用的,當(dāng)路由發(fā)生變化的時(shí)候,那么會(huì)調(diào)用快照函數(shù),來形成新的快照信息。
- 通過 popstate 監(jiān)聽 history 模式下的路由變化,路由變化的時(shí)候,執(zhí)行快照函數(shù),得到新的快照信息。
- 通過 useMutableSource ,把數(shù)據(jù)源對象,快照函數(shù),訂閱函數(shù)傳入,形成 pathName 。
- 可能這個(gè)例子,不足以讓你清楚 useMutableSource 的作用,我們再舉一個(gè)例子看一下 useMutableSource 如何和 redux 契合使用的。
例子二
例子二:redux 中 useMutableSource 使用
redux 可以通過 useMutableSource 編寫自定義 hooks —— useSelector,useSelector 可以讀取數(shù)據(jù)源的狀態(tài),當(dāng)數(shù)據(jù)源改變的時(shí)候,重新執(zhí)行快照獲取狀態(tài),做到訂閱更新。我們看一下 useSelector 是如何實(shí)現(xiàn)的。
- const mutableSource = createMutableSource(
- reduxStore, // 將 redux 的 store 作為數(shù)據(jù)源。
- // state 是不可變的,可以作為數(shù)據(jù)源的版本號(hào)
- () => reduxStore.getState()
- );
- // 通過創(chuàng)建 context 保存數(shù)據(jù)源 mutableSource。
- const MutableSourceContext = createContext(mutableSource);
- // 訂閱 store 變化。store 變化,執(zhí)行 getSnapshot
- const subscribe = (store, callback) => store.subscribe(callback);
- // 自定義 hooks useSelector 可以在每一個(gè) connect 內(nèi)部使用,通過 useContext 獲取 數(shù)據(jù)源對象。
- function useSelector(selector) {
- const mutableSource = useContext(MutableSourceContext);
- // 用 useCallback 讓 getSnapshot 變成有記憶的。
- const getSnapshot = useCallback(store => selector(store.getState()), [
- selector
- ]);
- // 最后本質(zhì)上用的是 useMutableSource 訂閱 state 變化。
- return useMutableSource(mutableSource, getSnapshot, subscribe);
- }
大致流程是這樣的:
- 將 redux 的 store 作為數(shù)據(jù)源對象 mutableSource 。state 是不可變的,可以作為數(shù)據(jù)源的版本號(hào)。
- 通過創(chuàng)建 context 保存數(shù)據(jù)源對象 mutableSource。
- 聲明訂閱函數(shù),訂閱 store 變化。store 變化,執(zhí)行 getSnapshot 。
- 自定義 hooks useSelector 可以在每一個(gè) connect 內(nèi)部使用,通過 useContext 獲取 數(shù)據(jù)源對象。用 useCallback 讓 getSnapshot 變成有記憶的。
- 最后本質(zhì)上用的是 useMutableSource 訂閱外部 state 變化。
注意問題
在創(chuàng)建 getSnapshot 的時(shí)候,需要將 getSnapshot 記憶化處理,就像上述流程中的 useCallback 處理 getSnapshot 一樣,如果不記憶處理,那么會(huì)讓組件頻繁渲染。
在最新的 react-redux 源碼中,已經(jīng)使用新的 api,訂閱外部數(shù)據(jù)源,不過不是 useMutableSource 而是 useSyncExternalStore,具體因?yàn)?useMutableSource 沒有提供內(nèi)置的 selectorAPI,需要每一次當(dāng)選擇器變化時(shí)候重新訂閱 store,如果沒有 useCallback 等 api 記憶化處理,那么將重新訂閱。具體內(nèi)容請參考 useMutableSource → useSyncExternalStore。
三 實(shí)踐
接下來我用一個(gè)例子來具體實(shí)踐一下 createMutableSource,讓大家更清晰流程。
這里還是采用 redux 和 createMutableSource 實(shí)現(xiàn)外部數(shù)據(jù)源的引用。這里使用的是 18.0.0-alpha 版本的 react 和 react-dom 。
- import React , {
- unstable_useMutableSource as useMutableSource,
- unstable_createMutableSource as createMutableSource
- } from 'react'
- import { combineReducers , createStore } from 'redux'
- /* number Reducer */
- function numberReducer(state=1,action){
- switch (action.type){
- case 'ADD':
- return state + 1
- case 'DEL':
- return state - 1
- default:
- return state
- }
- }
- /* 注冊reducer */
- const rootReducer = combineReducers({ number:numberReducer })
- /* 合成Store */
- const Store = createStore(rootReducer,{ number: 1 })
- /* 注冊外部數(shù)據(jù)源 */
- const dataSource = createMutableSource( Store ,() => 1 )
- /* 訂閱外部數(shù)據(jù)源 */
- const subscribe = (dataSource,callback)=>{
- const unSubScribe = dataSource.subscribe(callback)
- return () => unSubScribe()
- }
- /* TODO: 情況一 */
- export default function Index(){
- /* 獲取數(shù)據(jù)快照 */
- const shotSnop = React.useCallback((data) => ({...data.getState()}),[])
- /* hooks:使用 */
- const data = useMutableSource(dataSource,shotSnop,subscribe)
- return <div>
- <p> 擁抱 React 18 🎉🎉🎉 </p>
- 贊:{data.number} <br/>
- <button onClick={()=>Store.dispatch({ type:'ADD' })} >點(diǎn)贊</button>
- </div>
- }
第一部分用 combineReducers 和 createStore 創(chuàng)建 redux Store 的過程。
重點(diǎn)是第二部分:
- 首先通過 createMutableSource 創(chuàng)建數(shù)據(jù)源,Store 為數(shù)據(jù)源,data.getState() 作為版本號(hào)。
- 第二點(diǎn)就是快照信息,這里的快照就是 store 中的 state。所以在 shotSnop 還是通過 getState 獲取狀態(tài),正常情況下 shotSnop 應(yīng)該作為 Selector,這里把所有的 state 都映射出來了。
- 第三就是通過 useMutableSource 把數(shù)據(jù)源,快照,訂閱函數(shù)傳入,得到的 data 就是引用的外部數(shù)據(jù)源了。
接下來讓我們看一下效果:
四 原理分析
useMutableSource 已經(jīng)在 React v18 的規(guī)劃之中了,那么它的實(shí)現(xiàn)原理以及細(xì)節(jié),在 V18 正式推出之前可以還會(huì)有調(diào)整,
1 createMutableSource
react/src/ReactMutableSource.js -> createMutableSource
- function createMutableSource(source,getVersion){
- const mutableSource = {
- _getVersion: getVersion,
- _source: source,
- _workInProgressVersionPrimary: null,
- _workInProgressVersionSecondary: null,
- };
- return mutableSource
- }
createMutableSource 的原理非常簡單,和 createContext , createRef 類似, 就是創(chuàng)建一個(gè) createMutableSource 對象,
2 useMutableSource
對于 useMutableSource 原理也沒有那么玄乎,原來是由開發(fā)者自己把外部數(shù)據(jù)源注入到 state 中,然后寫訂閱函數(shù)。useMutableSource 的原理就是把開發(fā)者該做的事,自己做了??????,這樣省著開發(fā)者去寫相關(guān)的代碼了。本質(zhì)上就是 useState + useEffect :
- useState 負(fù)責(zé)更新。
- useEffect 負(fù)責(zé)訂閱。
然后來看一下原理。
react-reconciler/src/ReactFiberHooks.new.js -> useMutableSource
- function useMutableSource(hook,source,getSnapshot){
- /* 獲取版本號(hào) */
- const getVersion = source._getVersion;
- const version = getVersion(source._source);
- /* 用 useState 保存當(dāng)前 Snapshot,觸發(fā)更新。 */
- let [currentSnapshot, setSnapshot] = dispatcher.useState(() =>
- readFromUnsubscribedMutableSource(root, source, getSnapshot),
- );
- dispatcher.useEffect(() => {
- /* 包裝函數(shù) */
- const handleChange = () => {
- /* 觸發(fā)更新 */
- setSnapshot()
- }
- /* 訂閱更新 */
- const unsubscribe = subscribe(source._source, handleChange);
- /* 取消訂閱 */
- return unsubscribe;
- },[source, subscribe])
- }
上述代碼中保留了最核心的邏輯:
- 首先通過 getVersion 獲取數(shù)據(jù)源版本號(hào),用 useState 保存當(dāng)前 Snapshot,setSnapshot 用于觸發(fā)更新。
- 在 useEffect 中,進(jìn)行訂閱,綁定的是包裝好的 handleChange 函數(shù),里面調(diào)用 setSnapshot 真正的更新組件。
- 所以 useMutableSource 本質(zhì)上還是 useState 。
五 總結(jié)
今天講了 useMutableSource 的背景,用法,以及原理。希望閱讀的同學(xué)可以克隆一下 React v18 的新版本,嘗試一下新特性,將對理解 useMutableSource 很有幫助。下一章我們將繼續(xù)圍繞 React v18 展開。
參考文檔
useMutableSource RFC