<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>

          探尋 Redux useSelector 更新機制

          共 5877字,需瀏覽 12分鐘

           ·

          2021-01-13 11:25

          (給前端大全加星標(biāo),提升前端技能

          一個似乎無法實現(xiàn)的 hook 的內(nèi)部工作原理深入研究

          當(dāng)一個 react context 更新的時候,所有使用到該 context 的組件也會更新。但如果每次 redux store 一有變化,所有用到 react-redux 的useSelector的組件就重新 render,這就會導(dǎo)致一個很大的性能問題。那么useSelector是如何做到的呢?

          我是 React context API 的忠實粉絲,但是當(dāng) context 中的數(shù)據(jù)由兩部分組成,其中一個部分更新頻繁,而另一部分不常更新時,性能問題就出現(xiàn)了。即使我的組件只用到了不常更新的那部分數(shù)據(jù),組件還是會在另一部分數(shù)據(jù)更新的時候進行重新 render。

          react-redux 的useSelector hook 就沒有這樣的問題——組件只會在其所選擇到的(selected)數(shù)據(jù)更新的情況下才會重新 render,即使在 store 中的其他數(shù)據(jù)被更新的情況下也是這樣。那么這是什么原理呢?它是以某種方式繞過了 context 的規(guī)則?

          當(dāng)然,答案是否定的,但是useSelector 用到了一些有創(chuàng)意的手段達到了效果。在我們開始之前,讓我們先進步看下問題。這是一個有點牽強的例子,context 中的數(shù)據(jù)由兩部分組成,clicks 和 time :

          const?initialState?=?{
          ??clicks:?0,
          ??time:?0
          };

          //?Create?our?context
          const?MyContext?=?React.createContext(initialState);

          //?Increment?either?the?click?counter?or?the?timer
          const?reducer?=?(state,?action)?=>?{
          ??switch?(action.type)?{
          ????case?"CLICK":
          ??????return?{
          ????????...state,
          ????????clicks:?state.clicks?+?1
          ??????};
          ????case?"TIME":
          ??????return?{
          ????????...state,
          ????????time:?state.time?+?1
          ??????};
          ????default:
          ??????return?state;
          ??}
          };

          const?MyProvider?=?({?children?})?=>?{
          ??const?[state,?dispatch]?=?useReducer(reducer,?initialState);

          ??//?Kick?off?a?setInterval?that?increments?our?timer?each?second
          ??useEffect(()?=>?{
          ????const?interval?=?setInterval(()?=>?dispatch({?type:?"TIME"?}),?1000);
          ????return?()?=>?clearInterval(interval);
          ??},?[]);

          ??//?Memoize?our?context?value
          ??const?value?=?useMemo(
          ????()?=>?({
          ??????...state,
          ??????onClick:?()?=>?dispatch({?type:?"CLICK"?})
          ????}),
          ????[state]
          ??);

          ??return?<MyContext.Provider?value={value}>{children}MyContext.Provider>;
          };

          一部分的state(time)追蹤 app 打開后經(jīng)過的秒數(shù),另一部分(clicks)追蹤用戶點擊了多少次 button。我們可能在 app 中這么使用它:

          const?Clicker?=?()?=>?{
          ??console.log("render?clicker");
          ??const?{?clicks,?onClick?}?=?useContext(MyContext);
          ??return?(
          ????<div>
          ??????<span>{`Clicks:?${clicks}`}span>

          ??????<button?onClick={onClick}>Click?mebutton>
          ????div>
          ??);
          };

          const?Timer?=?()?=>?{
          ??console.log("render?timer");
          ??const?{?time?}?=?useContext(MyContext);
          ??return?(
          ????<div>
          ??????<span>{`Time:?${time}`}span>

          ????div>
          ??);
          };

          export?default?function?App()?{
          ??return?(
          ????<MyProvider>
          ??????<Clicker?/>
          ??????<Timer?/>
          ????MyProvider>

          ??);
          }

          timer 每秒鐘都會遞增,click 計數(shù)器只會在 button 被點擊后遞增。然而,因為 click 組件與 timer 組件都是用到了同一個 context,所以每次 timer 遞增的時候,click 組件也會重新 render,即使click狀態(tài)沒有變化!這很不好!

          比較容易讓人接受的解決方法是“把狀態(tài)分割到不同的context”,但制造這種人為的邊界感覺并不好。如果我在開發(fā)一個音頻播放器,我可不想被迫的將isPlayingcurrentTime拆開,只是因為currentTime常變而isPlaying不是。

          現(xiàn)在我們理解了這個問題,讓我們再來看看useSelector。如果我從頭實現(xiàn)react-redux,代碼可能會長這樣:

          //?Some?method?to?return?the?initial?state?of?your?redux?store
          const?initialState?=?getInitialState()?
          ?//?Returns?your?top-level?redux?reducer?
          const?myReducer?=?getCombinedReducers()

          const?MyContext?=?React.createContext(initialState)

          const?MyProvider?=?({?children?})?=>?{
          ??const?[store,?dispatch]?=?useReducer(myReducer,?initialState)
          ??
          ??//?Memoize?our?context?value.?getState?will?be?updated?each?time?the?store?updates.
          ??const?value?=?useMemo(()?=>?({
          ????getState:?()?=>?store
          ??}),?[store])
          ???
          ??return?(
          ????<MyContext.Provider?value={value}>
          ??????{children}
          ????MyContext.Provider>

          ??)
          }

          const?useSelector?=?(selector)?=>?{
          ??const?store?=?useContext(MyContext)
          ??return?selector(store.getState())
          }

          雖然我的useSelector能正確的返回store中的state,但它還是有著上述的問題。每次 timer更新的時候也會重新render。那么useSelector是怎么解決這個問題的呢。

          我不是react/redux的作者或者專家,但在翻看了源碼后,我認為我找到了答案:react-redux的context從沒有真正的改變。

          讓我澄清下。Redux有一個一直在變化的store,但我們是通過它的getState方法來獲取store中的state的。getState方法的引用沒有變化。getState()中返回的state可能改變了,但getter方法本身在整個app的生命周期中都沒有變化。

          這在react領(lǐng)域中通常是不允許的--我們一般是希望我們的組件在他們所依賴的狀態(tài)發(fā)生變化的時候能夠重新render。然而,就像我們剛才所見,這會導(dǎo)致不預(yù)期的render。

          那么在不能依賴其props/context的變化的情況下,一個組件如何知道該何時更新呢。另一種設(shè)計模式被使用:訂閱(subscription)。

          useSelector注冊了一個訂閱,每當(dāng)redux store更新時,該訂閱就會被調(diào)用,如果這次的更新導(dǎo)致了被選擇state的改變,就會觸發(fā)一次重新render并返回一個新值。訂閱是發(fā)生在這:

          subscription.onStateChange?=?checkForUpdates

          See where this happens in the react-redux code on GitHub[1]

          subscription會調(diào)用checkForUpdates,它會檢查對store的更新是否導(dǎo)致對選定狀態(tài)的更改。如果狀態(tài)改變了,一次重新render就會被forceRender({})觸發(fā):

          See where this happens in the react-redux code on GitHub[2]

          forceRender的唯一職責(zé)就是:強制重新render。他是通過遞增一個本地狀態(tài)來實現(xiàn)的,方法有些hacky但是很簡單,每次forceRender被調(diào)用,它便會遞增一個內(nèi)部計數(shù)器,計數(shù)器本身沒有被任何地方用到,但能達到預(yù)期效果。

          const?[,?forceRender]?=?useReducer(s?=>?s?+?1,?0)

          See where this happens in the react-redux code on GitHub[3]

          這個re-render反過來會導(dǎo)致useSelector從store中選擇出合適的狀態(tài),然后將其返回到我們的組件中:

          selectedState?=?selector(store.getState())
          //...
          return?selectedState

          See where this happens in the react-redux code on GitHub[4]

          總之,設(shè)置了一個每當(dāng)redux state改變時就會被觸發(fā)的訂閱。當(dāng)訂閱被觸發(fā)的時候,他會調(diào)用每一個useSelector中傳入的方法,然后判斷改方法使用(舊)store選擇出來的屬于與使用(新)store選擇出來的數(shù)據(jù)的內(nèi)部引用是否相等。如果不等,就會強制render,同時返回新的狀態(tài)。這樣對我來說感覺并不是很“reacty”,但它確實能達到效果。

          你可能已經(jīng)看出了個問題。如果我的選擇器返回的是一個多部分的組合狀態(tài),那么新創(chuàng)建的對象的引用永遠不會等于上次的引用,便又會回到過多重新render的老路上去。

          useSelector(state?=>?{
          ??//?Each?time?this?runs?it?returns?a?brand?new?object
          ??return?{
          ????thingOne:?state.thingOne
          ????thingTwo:?state.thingTwo
          ??}
          })

          這個問題可以通過給useSelector傳第二個參數(shù)(相等判斷方法),如來shallowEqual,來解決。參閱 該文檔。

          為了更好的衡量, 這里有個從零創(chuàng)建的,只有當(dāng)它的選擇狀態(tài)改變時才會觸發(fā)更新的,useSelectorhook 的最小實現(xiàn):

          https://codesandbox.io/s/peaceful-river-dj5ps 注意:雖然實現(xiàn)自己的useSelector是個有趣的練習(xí),但我不提倡在生產(chǎn)環(huán)境中使用


          備注1:我覺得訂閱者模式不“reacty”的原因是組件不在只是其“props and state“的結(jié)果。相反的,父級context必須保留每個的useSelector實例中的selector函數(shù)引用。

          備注2:我一直在用Kent C. Dodds的context模式并且非常喜歡--它對我們上述討論的問題沒有幫助,但是可以讓context的使用更友好。參考[5]

          參考資料

          [1]

          See where this happens in the react-redux code on GitHub: https://github.com/reduxjs/react-redux/blob/607f1ba30417b631a4df18665dfede416c7208cf/src/hooks/useSelector.js#L72

          [2]

          See where this happens in the react-redux code on GitHub: https://github.com/reduxjs/react-redux/blob/607f1ba30417b631a4df18665dfede416c7208cf/src/hooks/useSelector.js#L69

          [3]

          See where this happens in the react-redux code on GitHub: https://github.com/reduxjs/react-redux/blob/607f1ba30417b631a4df18665dfede416c7208cf/src/hooks/useSelector.js#L15

          [4]

          See where this happens in the react-redux code on GitHub: https://github.com/reduxjs/react-redux/blob/607f1ba30417b631a4df18665dfede416c7208cf/src/hooks/useSelector.js#L33

          [5]

          參考: https://kentcdodds.com/blog/how-to-use-react-context-effectively


          - EOF -


          推薦閱讀??點擊標(biāo)題可跳轉(zhuǎn)

          1、一步一步教你把 Redux Saga 添加到 React&Redux 程序中

          2、從零實現(xiàn) react redux

          3、不要再問React Hooks能否取代 Redux 了


          覺得本文對你有幫助?請分享給更多人

          推薦關(guān)注「前端大全」,提升前端技能

          點贊和在看就是最大的支持??

          瀏覽 94
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  大肠浣肠调教一区二区三区在线 | 国产18在线 | 16—17女人毛片毛片国内 | 8x8x国产一区二区三区精品痛苦 | 美国十次AV |