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


































