精讀《React Hooks 數據流》

1 引言

React Hooks 漸漸被國內前端團隊所接受,但基於 Hooks 的數據流方案卻還未固定,咱們有 「100 種」 相似的選擇,卻各有利弊,讓人難以取捨。前端

本週筆者就深刻談一談對 Hooks 數據流的理解,相信讀完文章後,能夠從百花齊放的 Hooks 數據流方案中看到本質。react

2 精讀

基於 React Hooks 談數據流,咱們先從最不容易產生分歧的基礎方案提及。git

單組件數據流

單組件最簡單的數據流必定是 useStategithub

function App() {
  const [count, setCount] = useState();
}
複製代碼

useState 在組件內用是毫無爭議的,那麼下個話題就必定是跨組件共享數據流了。redux

組件間共享數據流

跨組件最簡單的方案就是 useContext緩存

const CountContext = createContext();

function App() {
  const [count, setCount] = useState();
  return (
    <CountContext.Provider value={{ count, setCount }}> <Child /> </CountContext.Provider> ); } function Child() { const { count } = useContext(CountContext); } 複製代碼

用法都是官方 API,顯然也是毫無爭議的,但問題是數據與 UI 不解耦,這個問題 unstated-next 已經爲你想好解決方案了。微信

數據流與組件解耦

unstated-next 能夠幫你把上面例子中,定義在 App 中的數據單獨出來,造成一個自定義數據管理 Hook:ide

import { createContainer } from "unstated-next";

function useCounter() {
  const [count, setCount] = useState();
  return { count, setCount };
}

const Counter = createContainer(useCounter);

function App() {
  return (
    <Counter.Provider> <Child /> </Counter.Provider> ); } function Child() { const { count } = Counter.useContainer(); } 複製代碼

數據與 App 就解耦了,這下 Counter 不再和 App 綁定了,Counter 能夠和其餘組件綁定做用了。函數

這個時候性能問題就慢慢浮出了水面,首當其衝的就是 useState 沒法合併更新的問題,咱們天然想到利用 useReducer 解決。性能

合併更新

useReducer 可讓數據合併更新,這也是 React 官方 API,毫無爭議:

import { createContainer } from "unstated-next";

function useCounter() {
  const [state, dispath] = useReducer(
    (state, action) => {
      switch (action.type) {
        case "setCount":
          return {
            ...state,
            count: action.setCount(state.count),
          };
        case "setFoo":
          return {
            ...state,
            foo: action.setFoo(state.foo),
          };
        default:
          return state;
      }
      return state;
    },
    { count: 0, foo: 0 }
  );

  return { ...state, dispatch };
}

const Counter = createContainer(useCounter);

function App() {
  return (
    <Counter.Provider> <Child /> </Counter.Provider> ); } function Child() { const { count } = Counter.useContainer(); } 複製代碼

這下即使要同時更新 countfoo,咱們也能經過抽象成一個 reducer 的方式合併更新。

然而還有性能問題:

function ChildCount() {
  const { count } = Counter.useContainer();
}

function ChildFoo() {
  const { foo } = Counter.useContainer();
}
複製代碼

更新 foo 時,ChildCountChildFoo 同時會執行,但 ChildCount 沒用到 foo 呀?這個緣由是 Counter.useContainer 提供的數據流是一個引用總體,其子節點 foo 引用變化後會致使整個 Hook 從新執行,繼而全部引用它的組件也會從新渲染。

此時咱們發現能夠利用 Redux useSelector 實現按需更新。

按需更新

首先咱們利用 Redux 對數據流作一次改造:

import { createStore } from "redux";
import { Provider, useSelector } from "react-redux";

function reducer(state, action) {
  switch (action.type) {
    case "setCount":
      return {
        ...state,
        count: action.setCount(state.count),
      };
    case "setFoo":
      return {
        ...state,
        foo: action.setFoo(state.foo),
      };
    default:
      return state;
  }
  return state;
}

function App() {
  return (
    <Provider store={store}> <Child /> </Provider>
  );
}

function Child() {
  const { count } = useSelector(
    (state) => ({ count: state.count }),
    shallowEqual
  );
}
複製代碼

useSelector 可讓 Childcount 變化時才更新,而 foo 變化時不更新,這已經接近較爲理想的性能目標了。

useSelector 的做用僅僅是計算結果不變化時阻止組件刷新,但並不能保證返回結果的引用不變化。

防止數據引用頻繁變化

對於上面的場景,拿到 count 的引用是不變的,但對於其餘場景就不必定了

舉個例子:

function Child() {
  const user = useSelector((state) => ({ user: state.user }), shallowEqual);

  return <UserPage user={user} />; } 複製代碼

假設 user 對象在每次數據流更新引用都會發生變化,那麼 shallowEqual 天然是不起做用,那咱們換成 deepEqual深對比呢?結果是引用依然會變,只是重渲染不那麼頻繁了:

function Child() {
  const user = useSelector(
    (state) => ({ user: state.user }),
    // 當 user 值變化時才重渲染
    deepEqual
  );

  // 但此處拿到的 user 引用仍是會變化

  return <UserPage user={user} />; } 複製代碼

是否是以爲在 deepEqual 的做用下,沒有觸發重渲染,user 的引用就不會變呢?答案是會變,由於 user 對象在每次數據流更新都會變,useSelectordeepEqual 做用下沒有觸發重渲染,但由於全局 reducer 隱去組件本身的重渲染依然會從新執行此函數,此時拿到的 user 引用會不斷變化。

所以 useSelector deepEqual 必定要和 useDeepMemo 結合使用,才能保證 user 引用不會頻繁改變:

function Child() {
  const user = useSelector(
    (state) => ({ user: state.user }),
    // 當 user 值變化時才重渲染
    deepEqual
  );

  const userDeep = useDeepMemo(() => user, [user]);

  return <UserPage user={user} />; } 複製代碼

固然這是比較極端的狀況,只要看到 deepEqualuseSelector 同時做用了,就要問問本身其返回的值的引用會不會發生意外變化。

緩存查詢函數

對於極限場景,即使控制了重渲染次數與返回結果的引用最大程度不變,仍是可能存在性能問題,這最後一塊性能問題就處在查詢函數上。

上面的例子中,查詢函數比較簡單,但若是查詢函數很是複雜就不同了:

function Child() {
  const user = useSelector(
    (state) => ({ user: verySlowFunction(state.user) }),
    // 當 user 值變化時才重渲染
    deepEqual
  );

  const userDeep = useDeepMemo(() => user, [user]);

  return <UserPage user={user} />; } 複製代碼

咱們假設 verySlowFunction 要遍歷畫布中 1000 個組件的 n 3 次方次,那組件的重渲染時間消耗與查詢時間相比徹底不值一提,咱們須要考慮緩存查詢函數。

一種方式是利用 reselect 根據參數引用進行緩存。

想象一下,若是 state.user 的引用不頻繁變化,但 verySlowFunction 很是慢,理想狀況是 state.user 引用變化後才從新執行 verySlowFunction,但上面的例子中,useSelector 並不知道還能這麼優化,只能傻傻的每次渲染重複執行 verySlowFunction,哪怕 state.user 沒有變。

此時咱們要告訴引用,state.user 是否變化纔是從新執行的關鍵:

import { createSelector } from "reselect";

const userSelector = createSelector(
  (state) => state.user,
  (user) => verySlowFunction(user)
);

function Child() {
  const user = useSelector(
    (state) => userSelector(state),
    // 當 user 值變化時才重渲染
    deepEqual
  );

  const userDeep = useDeepMemo(() => user, [user]);

  return <UserPage user={user} />; } 複製代碼

在上面的例子中,經過 createSelector 建立的 userSelector 會一層層進行緩存,當第一個參數返回的 state.user 引用不變時,會直接返回上一次執行結果,直到其應用變化了纔會繼續往下執行。

這也說明了函數式保持冪等的重要性,若是 verySlowFunction 不是嚴格冪等的,這種緩存也沒法實施。

看上去很美好,然而實戰中你可能發現沒有那麼美好,由於上面的例子都創建在 Selector 徹底不依賴外部變量

結合外部變量的緩存查詢

若是咱們要查詢的用戶來自於不一樣地區,須要傳遞 areaId 加以識別,那麼能夠拆分爲兩個 Selector 函數:

import { createSelector } from "reselect";

const areaSelector = (state, props) => state.areas[props.areaId].user;

const userSelector = createSelector(areaSelector, (user) =>
  verySlowFunction(user)
);

function Child() {
  const user = useSelector(
    (state) => userSelector(state, { areaId: 1 }),
    deepEqual
  );

  const userDeep = useDeepMemo(() => user, [user]);

  return <UserPage user={user} />; } 複製代碼

因此爲了避免在組件函數內調用 createSelector,咱們須要儘量將用到外部變量的地方抽象成一個通用 Selector,並做爲 createSelector 的一個先手環節。

userSelector 提供給多個組件使用時緩存會失效,緣由是咱們只建立了一個 Selector 實例,所以這個函數還須要再包裝一層高階形態:

import { createSelector } from "reselect";

const userSelector = () =>
  createSelector(areaSelector, (user) => verySlowFunction(user));

function Child() {
  const customSelector = useMemo(userSelector, []);

  const user = useSelector(
    (state) => customSelector(state, { areaId: 1 }),
    deepEqual
  );
}
複製代碼

因此對於外部變量結合的環節,還須要 useMemouseSelector 結合使用,useMemo 處理外部變量依賴的引用緩存,useSelector 處理 Store 相關引用緩存。

3 總結

基於 Hooks 的數據流方案不能算完美,我在寫做這篇文章時就感受到這種方案屬於 「淺入深出」,簡單場景還容易理解,隨着場景逐步複雜,方案也變得愈來愈複雜。

但這種 Immutable 的數據流管理思路給了開發者很是自由的緩存控制能力,只要透徹理解上述概念,就能夠開發出很是 「符合預期」 的數據緩存管理模型,只要精心維護,一切就變得很是有秩序。

討論地址是:精讀《React Hooks 數據流》 · Issue #242 · dt-fe/weekly

若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公衆號

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章
相關標籤/搜索