<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          「React18新特性」深度解讀之useMutableSource

          共 14557字,需瀏覽 30分鐘

           ·

          2022-06-18 19:40

          一 前言

          大家好,我是 ?? ,接下來(lái)會(huì)出一個(gè)新系列,React v18新特性解讀,主要針對(duì)新特性的產(chǎn)生背景功能介紹,和原理分析等幾個(gè)方面,勇于做第一個(gè)吃螃蟹的人。希望支持我的朋友可以點(diǎn)贊轉(zhuǎn)發(fā)再看,關(guān)注一波公眾號(hào),持續(xù)分享前端技術(shù)硬文。

          useMutableSource 最早的 RFC 提案在 2020年 2 月份就開(kāi)始了。在 React 18 中它將作為新特性出現(xiàn)。用一段提案中的描述來(lái)概括 useMutableSource

          useMutableSource 能夠讓 React 組件在 Concurrent Mode 模式下安全地有效地讀取外接數(shù)據(jù)源,在組件渲染過(guò)程中能夠檢測(cè)到變化,并且在數(shù)據(jù)源發(fā)生變化的時(shí)候,能夠調(diào)度更新。

          說(shuō)起外部數(shù)據(jù)源就要從 state 和更新說(shuō)起 ,無(wú)論是 React 還是 Vue 這種傳統(tǒng) UI 框架中,雖然它們都采用虛擬 DOM 方式,但是還是不能夠把更新單元委托到虛擬 DOM 身上來(lái),所以更新的最小粒度還是在組件層面上,由組件統(tǒng)一管理數(shù)據(jù) state,并參與調(diào)度更新。

          回到我們的主角 React 上,既然由組件 component 管控著狀態(tài) state。那么在 v17 和之前的版本,React 想要視圖上的更新,那么只能通過(guò)更改內(nèi)部數(shù)據(jù) state 。縱覽 React 的幾種更新方式,無(wú)一離不開(kāi)自身 state 。先來(lái)看一下 React 的幾種更新模式。

          • 組件本身改變 state 。函數(shù) useState | useReducer ,類(lèi)組件 setState | forceUpdate
          • props 改變,由組件更新帶來(lái)的子組件的更新。
          • context 更新,并且該組件消費(fèi)了當(dāng)前 context

          無(wú)論是上面哪種方式,本質(zhì)上都是 state 的變化。

          • props 改變來(lái)源于父級(jí)組件的 state 變化。
          • context 變化來(lái)源于 Provider 中 value 變化,而 value 一般情況下也是 state 或者是 state 衍生產(chǎn)物。

          從上面可以概括出:state和視圖更新的關(guān)系 Model => View 。但是 state 僅限于組件內(nèi)部的數(shù)據(jù),如果 state 來(lái)源于外部(脫離組件層面)。那么如何完成外部數(shù)據(jù)源轉(zhuǎn)換成內(nèi)部狀態(tài), 并且數(shù)據(jù)源變化,組件重新 render 呢?

          常規(guī)模式下,先把外部數(shù)據(jù) external Data 通過(guò) selector 選擇器把組件需要的數(shù)據(jù)映射到 state | props 上。這算是完成了一步,接下來(lái)還需要 subscribe 訂閱外部數(shù)據(jù)源的變化,如果發(fā)生變化,那么還需要自身去強(qiáng)制更新 forceUpdate 。下面兩幅圖表示數(shù)據(jù)注入和數(shù)據(jù)訂閱更新。

          1.jpg
          2.jpg

          典型的外部數(shù)據(jù)源就是 redux 中的 store ,redux 是如何把 Store 中的 state ,安全的變成組件的 state 的。

          或許我可以用一段代碼來(lái)表示從 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>
          }

          但是例子中代碼,沒(méi)有實(shí)際意義,也不是源代碼,我這里就是讓大家清晰地了解流程。redux 和 react 本質(zhì)上是這樣工作的。

          • 通過(guò) store.subscribe 來(lái)訂閱 state 變化,但是本質(zhì)上要比代碼片段中復(fù)雜的多,通過(guò) 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)。對(duì)于細(xì)節(jié),無(wú)關(guān)緊要,因?yàn)榻裉熘攸c(diǎn)是 useMutableSource

          如上是沒(méi)有 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)
          }
          • 通過(guò) createMutableSource 創(chuàng)建外部數(shù)據(jù)源,通過(guò) useMutableSource 來(lái)使用外部數(shù)據(jù)源。外部數(shù)據(jù)源變化,組件自動(dòng)渲染。

          如上是通過(guò) useMutableSource 實(shí)現(xiàn)的訂閱更新,這樣減少了 APP 內(nèi)部組件代碼,代碼健壯性提升,一定程度上也降低了耦合。接下來(lái)讓我們?nèi)矫嬲J(rèn)識(shí)一下這個(gè) V18 的新特性。

          二 功能介紹

          具體功能介紹流程還是參考最新的 RFC, createMutableSource 和 useMutableSource 在一定的程度上,有點(diǎn)像 createContextuseContext ,見(jiàn)名知意,就是創(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 類(lèi)似。(沒(méi)有使用過(guò) useSubscription 的同學(xué),可以了解一下 )。

          先來(lái)看一下 useMutableSource 的基本使用:

          const value = useMutableSource(source,getSnapShot,subscribe)

          useMutableSource 是一個(gè) hooks ,它有三個(gè)參數(shù):

          • source:MutableSource < Source > 可以理解為帶記憶的數(shù)據(jù)源對(duì)象。
          • getSnapshot:( source : Source ) => Snapshot :一個(gè)函數(shù),數(shù)據(jù)源作為函數(shù)的參數(shù),獲取快照信息,可以理解為 selector ,把外部的數(shù)據(jù)源的數(shù)據(jù)過(guò)濾,找出想要的數(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 功能類(lèi)似:

          • 兩者都需要帶有記憶化的‘配置化對(duì)象’,從而從外部取值。
          • 兩者都需要一種訂閱和取消訂閱源的方法 subscribe

          除此之外 useMutableSource 還有一些特點(diǎn):

          • useMutableSource 需要源作為顯式參數(shù)。也就是需要把數(shù)據(jù)源對(duì)象作為第一個(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)通過(guò) getSnapshot 讀取外部數(shù)據(jù)源的時(shí)候,返回的 value 應(yīng)該是不可變的。

          • ? 正確寫(xiě)法:getSnapshot: source => Array.from(source.friendIDs)
          • ? 錯(cuò)誤寫(xiě)法:getSnapshot: source => source.friendIDs

          數(shù)據(jù)源必須有一個(gè)全局的版本號(hào),這個(gè)版本號(hào)代表整個(gè)數(shù)據(jù)源:

          • ? 正確寫(xiě)法:getVersion: () => source.version
          • ? 錯(cuò)誤寫(xiě)法:getVersion: () => source.user.version

          接下來(lái)參考 github 上的例子,我講一下具體怎么使用:

          例子一

          例子一:訂閱 history 模式下路由變化

          比如有一個(gè)場(chǎng)景就是在非人為情況下,訂閱路由變化,展示對(duì)應(yīng)的 location.pathname,看一下是如何使用 useMutableSource 處理的。在這種場(chǎng)景下,外部數(shù)據(jù)源就是 location 信息。

          // 通過(guò) createMutableSource 創(chuàng)建一個(gè)外部數(shù)據(jù)源。
          // 數(shù)據(jù)源對(duì)象為 window。
          // 用 location.href 作為數(shù)據(jù)源的版本號(hào),href 發(fā)生變化,那么說(shuō)明數(shù)據(jù)源發(fā)生變化。
          const locationSource = createMutableSource(
            window,
            () => window.location.href
          );

          // 獲取快照信息,這里獲取的是 location.pathname 字段,這個(gè)是可以復(fù)用的,當(dāng)路由發(fā)生變化的時(shí)候,那么會(huì)調(diào)用快照函數(shù),來(lái)形成新的快照信息。
          const getSnapshot = window => window.location.pathname

          // 訂閱函數(shù)。
          const subscribe = (window, callback) => {
             //通過(guò) popstate 監(jiān)聽(tīng) history 模式下的路由變化,路由變化的時(shí)候,執(zhí)行快照函數(shù),得到新的快照信息。
            window.addEventListener("popstate", callback);
             //取消監(jiān)聽(tīng)
            return () => window.removeEventListener("popstate", callback);
          };

          function Example({
            // 通過(guò) useMutableSource,把數(shù)據(jù)源對(duì)象,快照函數(shù),訂閱函數(shù)傳入,形成 pathName。  
            const pathName = useMutableSource(locationSource, getSnapshot, subscribe);

            // ...
          }

          來(lái)描繪一下流程:

          • 首先通過(guò) createMutableSource 創(chuàng)建一個(gè)數(shù)據(jù)源對(duì)象,該數(shù)據(jù)源對(duì)象為 window。用 location.href 作為數(shù)據(jù)源的版本號(hào),href 發(fā)生變化,那么說(shuō)明數(shù)據(jù)源發(fā)生變化。
          • 獲取快照信息,這里獲取的是 location.pathname 字段,這個(gè)是可以復(fù)用的,當(dāng)路由發(fā)生變化的時(shí)候,那么會(huì)調(diào)用快照函數(shù),來(lái)形成新的快照信息。
          • 通過(guò) popstate 監(jiān)聽(tīng) history 模式下的路由變化,路由變化的時(shí)候,執(zhí)行快照函數(shù),得到新的快照信息。
          • 通過(guò) useMutableSource ,把數(shù)據(jù)源對(duì)象,快照函數(shù),訂閱函數(shù)傳入,形成 pathName

          可能這個(gè)例子??,不足以讓你清楚 useMutableSource 的作用,我們?cè)倥e一個(gè)例子看一下 useMutableSource 如何和 redux 契合使用的。

          例子二

          例子二:redux 中 useMutableSource 使用

          redux 可以通過(guò) useMutableSource 編寫(xiě)自定義 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()
          );

          // 通過(guò)創(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)部使用,通過(guò) useContext 獲取 數(shù)據(jù)源對(duì)象。 
          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ù)源對(duì)象 mutableSource 。state 是不可變的,可以作為數(shù)據(jù)源的版本號(hào)。
          • 通過(guò)創(chuàng)建 context 保存數(shù)據(jù)源對(duì)象 mutableSource
          • 聲明訂閱函數(shù),訂閱 store 變化。store 變化,執(zhí)行 getSnapshot
          • 自定義 hooks useSelector 可以在每一個(gè) connect 內(nèi)部使用,通過(guò) useContext 獲取 數(shù)據(jù)源對(duì)象。用 useCallback 讓 getSnapshot 變成有記憶的。
          • 最后本質(zhì)上用的是 useMutableSource 訂閱外部 state 變化。

          注意問(wèn)題

          • 在創(chuàng)建 getSnapshot 的時(shí)候,需要將 getSnapshot 記憶化處理,就像上述流程中的 useCallback 處理 getSnapshot 一樣,如果不記憶處理,那么會(huì)讓組件頻繁渲染。
          • 在最新的 react-redux 源碼中,已經(jīng)使用新的 api,訂閱外部數(shù)據(jù)源,不過(guò)不是 useMutableSource 而是 useSyncExternalStore,具體因?yàn)?useMutableSource 沒(méi)有提供內(nèi)置的 selectorAPI,需要每一次當(dāng)選擇器變化時(shí)候重新訂閱 store,如果沒(méi)有 useCallback 等 api 記憶化處理,那么將重新訂閱。具體內(nèi)容請(qǐng)參考 useMutableSource → useSyncExternalStore。

          三 實(shí)踐

          接下來(lái)我用一個(gè)例子來(lái)具體實(shí)踐一下 createMutableSource,讓大家更清晰流程。

          這里還是采用 redux 和 createMutableSource 實(shí)現(xiàn)外部數(shù)據(jù)源的引用。這里使用的是 18.0.0-alpha 版本的 reactreact-dom

          3.jpg
          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
              }
          }
          /* 注冊(cè)reducer */
          const rootReducer = combineReducers({ number:numberReducer  })
          /* 合成Store */
          const Store = createStore(rootReducer,{ number1  })
          /* 注冊(cè)外部數(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>

          }

          第一部分用 combineReducerscreateStore 創(chuàng)建 redux Store 的過(guò)程。
          重點(diǎn)是第二部分:

          • 首先通過(guò) createMutableSource 創(chuàng)建數(shù)據(jù)源,Store 為數(shù)據(jù)源,data.getState() 作為版本號(hào)。
          • 第二點(diǎn)就是快照信息,這里的快照就是 store 中的 state。所以在 shotSnop 還是通過(guò) getState 獲取狀態(tài),正常情況下 shotSnop 應(yīng)該作為 Selector,這里把所有的 state 都映射出來(lái)了。
          • 第三就是通過(guò) useMutableSource 把數(shù)據(jù)源,快照,訂閱函數(shù)傳入,得到的 data 就是引用的外部數(shù)據(jù)源了。

          接下來(lái)讓我們看一下效果:

          4.gif

          四 原理分析

          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,
                  _workInProgressVersionPrimarynull,
                  _workInProgressVersionSecondarynull,
              };
              return mutableSource
          }

          createMutableSource 的原理非常簡(jiǎn)單,和 createContextcreateRef 類(lèi)似, 就是創(chuàng)建一個(gè) createMutableSource 對(duì)象,

          2 useMutableSource

          對(duì)于 useMutableSource 原理也沒(méi)有那么玄乎,原來(lái)是由開(kāi)發(fā)者自己把外部數(shù)據(jù)源注入到 state 中,然后寫(xiě)訂閱函數(shù)。useMutableSource 的原理就是把開(kāi)發(fā)者該做的事,自己做了??????,這樣省著開(kāi)發(fā)者去寫(xiě)相關(guān)的代碼了。本質(zhì)上就是 useState + useEffect

          • useState 負(fù)責(zé)更新。
          • useEffect 負(fù)責(zé)訂閱。

          然后來(lái)看一下原理。

          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])
          }

          上述代碼中保留了最核心的邏輯:

          • 首先通過(guò) 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 的新版本,嘗試一下新特性,將對(duì)理解 useMutableSource 很有幫助。下一章我們將繼續(xù)圍繞 React v18 展開(kāi)。

          參考文檔

          • useMutableSource RFC


          往期推薦


          什么時(shí)候不能使用箭頭函數(shù)?
          從零到一建立一套完整的前端規(guī)范
          前端一個(gè)月面試小記,字節(jié)、螞蟻、美團(tuán)、滴滴

          最后


          • 歡迎加我微信,拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專(zhuān)業(yè)的技術(shù)人...

          點(diǎn)個(gè)在看支持我吧
          瀏覽 45
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲黄片在线播放 | 一本大道久久久久 | 欧美国产精品一区 | 操学生妹在线播放 | 在线视频中文字幕亚洲 |