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

          教你寫一個 React 狀態(tài)管理庫

          共 8020字,需瀏覽 17分鐘

           ·

          2021-11-15 20:01

          自從 React Hooks 推行后,Redux 作為狀態(tài)管理方案就顯得格格不入了。Dan Abramov 很早就提到過 “You might not need Redux”,開發(fā)者必須要寫很多“模式代碼”,繁瑣以及重復是開發(fā)者不愿意容忍的。除了 actions/reducers/store 等概念對新手不夠友好之外,最大的缺點就是它對 typescript 類型支持太爛,在大型項目中這是不可接受的。

          通過對 Redux 的優(yōu)缺點總結來看,我們可以自己寫一個狀態(tài)管理庫,本次需要達到的目的:

          1. typescript 類型要完善
          2. 足夠簡單,概念要少
          3. React Hooks 要搭配

          因此,閱讀此文檔的前提要對 React Hookstypescript 等有一定的概念。OK, 那我們開始吧。

          思路

          目前流行的狀態(tài)管理庫很多都太復雜了,夾雜著大量的概念及 API,我們需要規(guī)劃著如何實現(xiàn)它。狀態(tài)管理也就是狀態(tài)提升的極致表現(xiàn)。我們的目的是要足夠簡單, API 少。 思考一下,我們是否可以考慮用 Context 去穿透做管理,用最基本的 useStatehooks 做狀態(tài)存儲,那么就嘗試下吧。

          這是三個最簡單的函數(shù)式組件 Demo,我們用它來試驗:

          function App() {
          return <Card />;
          }

          function Card() {
          return <CardBody />;
          }

          function CardBody() {
          return <div>Textdiv>;
          }

          實現(xiàn)

          我們定義 Context,一個很基本的狀態(tài)模型

          // 描述 Context 的類型
          interface IStoreContext {
          count: number;
          setCount: React.Dispatch>;
          increment: () => void;
          decrement: () => void;
          }

          // 創(chuàng)建一個 Context,無需默認值,這里為了演示方便,用了斷言
          export const StoreContext = React.createContext(undefined as unknown as IStoreContext);

          以及定義基本的 state,并配合 Context

          function App() {
          // 定義狀態(tài)
          const [count, setCount] = React.useState(0);
          const increment = () => setCount(count + 1);
          const decrement = () => setCount(count - 1);

          // 包裹 Provider,所有子組件都能拿到 context 的值
          return (
          <StoreContext.Provider value={{ count, setCount, increment, decrement }}>
          <Card />
          StoreContext.Provider>

          );
          }

          接下來我們在 CardBody 中使用這個 Context,使其穿透取值

          function CardBody() {
          // 獲取外層容器中的狀態(tài)
          const store = React.useContext(StoreContext);

          return <div onClick={store.increment}>Text {store.count}div>;
          }

          這樣,一個最簡單的穿透狀態(tài)管理的代碼寫好了。發(fā)現(xiàn)問題了嗎,狀態(tài)的業(yè)務邏輯寫在了 App 組件里,這個代碼耦合度太高了!我們來整理一下,需要將 App 的狀態(tài)通過自定義 hook 抽離出去,保持邏輯與組件的純粹性。

          // 將 App 中的狀態(tài)用自定義 hook 管理,邏輯和組件的表現(xiàn)抽離
          function useStore() {
          // 定義狀態(tài)
          const [count, setCount] = React.useState(0);
          const increment = () => setCount(count + 1);
          const decrement = () => setCount(count - 1);

          return {
          count,
          setCount,
          increment,
          decrement,
          };
          }

          App 中使用這個 hook

          function App() {
          const store = useStore();

          return (
          <StoreContext.Provider value={store}>
          <Card />
          StoreContext.Provider>

          );
          }

          現(xiàn)在來看是不是舒心多了,邏輯在單獨的 hook 中控制,具有高內聚的特點。想想也許還不夠,useStoreStoreContext 的邏輯還不夠內聚,繼續(xù):

          useStoreStoreContext.Provider 抽離成一個組件

          const Provider: React.FC = ({ children }) => {
          const store = useStore();
          return <StoreContext.Provider value={store}>{children}StoreContext.Provider>;
          };

          再來看 App 組件,是不是很清晰?

          function App() {
          return (
          <StoreProvider>
          <Card />
          StoreProvider>

          );
          }

          好了,我們可以將這個模式封裝成一個方法,通過工廠模式來創(chuàng)建 ContextProvider

          // 將自定義 Hook 通過參數(shù)傳入
          // 定義泛型描述 Context 形狀
          export function createContainer<Value, State = void>(useHook: (initialState?: State) => Value) {
          const Context = React.createContext(undefined as unknown as Value);

          const Provider: React.FC<{ initialState?: State }> = ({ initialState, children }) => {
          // 使用外部傳入的 hook
          const value = useHook(initialState);
          return <Context.Provider value={value}>{children}Context.Provider>;
          };

          return { Provider, Context };
          }

          OK,一個簡單的狀態(tài)管理算成型了。好不好用我們來試試,將之前定義的 useStore 的代碼移入 createContainer

          export const BaseStore = createContainer(() => {
          // 定義狀態(tài)
          const [count, setCount] = React.useState(0);
          const increment = () => setCount(count + 1);
          const decrement = () => setCount(count - 1);

          return {
          count,
          setCount,
          increment,
          decrement,
          };
          });

          App 中替換為 BaseStore 導出的 Provider

          function App() {
          return (
          <BaseStore.Provider>
          <Card />
          BaseStore.Provider>

          );
          }

          CardBody 使用 BaseStore 導出的 Context,因為定義的時候用了泛型,這里能完美識別當前 store 的形狀,從而具備編輯器智能提示

          function CardBody() {
          const store = React.useContext(BaseStore.Context);

          return <div onClick={store.increment}>Text {store.count}div>;
          }

          那么恭喜你,你已經(jīng)創(chuàng)建了一個屬于自己的狀態(tài)管理庫,我們給它取個名字叫 unstated-next

          調整

          但是方便和性能總是有所取舍的,毫無疑問,成也 Context,敗也 Context。因為它的穿透無差別更新特性也就決定了會讓所有的 React.memo 優(yōu)化失效。一次 setState 幾乎讓整個項目跟著 rerender ,這是極為不可接受的。因為自定義 Hook 每次執(zhí)行返回的都是一個全新的對象,那么 Provider 每次都會接受到這個全新的對象。所有用到這個 Context 的子組件都跟著一起更新,造成無意義的損耗調用。

          有方案嗎?想一想,辦法總比困難多。我們可以優(yōu)選 Context 上下文的特性,放棄導致重新渲染的特性(即每次傳給他一個固定引用)。這樣的話狀態(tài)改變,該更新的子組件不跟著更新了怎么辦,有什么辦法觸發(fā) rerender 呢?答案是 setState,我們可以將 setState 方法提升到 Context 里,讓容器去調度調用更新。

          // createContainer 函數(shù)中

          // 首先我們可以將 Context 設置為不觸發(fā) render
          // 這里 createContext 第二個參數(shù)的函數(shù)返回值為 0 即為不觸發(fā) render
          // 注意:這個 API 非正規(guī)。當然也可以用 useRef 轉發(fā)整個 Context 的值,使其不可變
          // 用非正規(guī)的 API 僅僅只是為了不用 ref 而少點代碼 ??
          const Context = React.createContext(undefined as unknown as Value, () => 0);

          那現(xiàn)在 Context 已經(jīng)是不可變了,該如何實現(xiàn)更新邏輯呢?思路可以是這樣:我們在子組件 mount 時添加一個 listenerContext 中,unMount 時將其移除,Context 有更新時,調用這個 listener,使其 rerender

          聲明一個 Context,用來放這些子組件的 listener

          // createContainer 函數(shù)中
          const ListenerContext = React.createContext<Set<(value: Value) => void>>(new Set());

          現(xiàn)在子組件中需要這樣一個 hook,想選擇 store 里的某些狀態(tài)去使用,無相關的 state 改變不用通知我更新。

          那么我們就起個名字叫 useSelector,用來監(jiān)聽哪些值變化可以讓本組件 rerender

          函數(shù)類型可以這樣定義:通過傳入一個函數(shù),來手動指定需要監(jiān)聽的值,并返回這個值

          // createContainer 函數(shù)中

          function useSelector<Selected>(selector: (value: Value) => Selected): Selected {}

          那我們來實現(xiàn)這個 useSelector 。首先是觸發(fā) rerender 的方法,這里用 reducer 讓其內部自增,調用時不用傳參數(shù)

          const [, forceUpdate] = React.useReducer((c) => c + 1, 0);

          這里我們需要和容器中的 Context 通信,從而獲取所有狀態(tài)傳給 selector 函數(shù)

          // 這里的 Context 已經(jīng)不具備觸發(fā)更新的特性
          const value = React.useContext(Context);
          const listeners = React.useContext(ListenerContext);

          // 調用方法獲取選擇的值
          const selected = selector(value);

          創(chuàng)建 listener 函數(shù),通過 Ref 轉發(fā),將選擇后的 state 提供給 listener 函數(shù)使用, 讓這個函數(shù)能拿到最新的 state

          const StoreValue = {
          selector,
          value,
          selected,
          };
          const ref = React.useRef(StoreValue);

          ref.current = StoreValue;

          實現(xiàn)這個 listener 函數(shù)

          function listener(nextValue: Value) {
          try {
          const refValue = ref.current;
          // 如果前后對比的值一樣,則不觸發(fā) render
          if (refValue.value === nextValue) {
          return;
          }
          // 將選擇后的值進行淺對比,一樣則不觸發(fā) render
          const nextSelected = refValue.selector(nextValue);
          //
          if (isShadowEqual(refValue.selected, nextSelected)) {
          return;
          }
          } catch (e) {
          // ignore
          }
          // 運行到這里,說明值已經(jīng)變了,觸發(fā) render
          forceUpdate();
          }

          我們需要在組件 mount/Unmount 的時候添加/移除 listener

          React.useLayoutEffect(() => {
          listeners.add(listener);
          return () => {
          listeners.delete(listener);
          };
          }, []);

          完整實現(xiàn)如下:

          function useSelector<Selected>(selector: (value: Value) => Selected): Selected {
          const [, forceUpdate] = React.useReducer((c) => c + 1, 0);

          const value = React.useContext(Context);
          const listeners = React.useContext(ListenerContext);

          const selected = selector(value);

          const StoreValue = {
          selector,
          value,
          selected,
          };
          const ref = React.useRef(StoreValue);

          ref.current = StoreValue;

          React.useLayoutEffect(() => {
          function listener(nextValue: Value) {
          try {
          const refValue = ref.current;
          if (refValue.value === nextValue) {
          return;
          }
          const nextSelected = refValue.selector(nextValue);
          if (isShadowEqual(refValue.selected, nextSelected)) {
          return;
          }
          } catch (e) {
          // ignore
          }
          forceUpdate();
          }

          listeners.add(listener);
          return () => {
          listeners.delete(listener);
          };
          }, []);
          return selected;
          }

          有了 selector。最后一步,我們來改寫 Provider

          // createContainer 函數(shù)中

          const Provider: React.FC<{ initialState?: State }> = ({ initialState, children }) => {
          const value = useHook(initialState);
          // 使用 Ref,讓 listener Context 不具備觸發(fā)更新
          const listeners = React.useRef<Set<(listener: Value) => void>>(new Set()).current;

          // 每次 useHook 里面 setState 就會讓本組件更新,使 listeners 觸發(fā)調用,從而使改變狀態(tài)的子組件 render
          listeners.forEach((listener) => {
          listener(value);
          });
          return (
          <Context.Provider value={value}>
          <ListenerContext.Provider value={listeners}>{children}ListenerContext.Provider>

          Context.Provider>
          );
          };

          大功告成!useSelector 返回的新對象都會如同 React.memo 一樣做淺對比。API 用法也如同 React-Redux,毫無學習成本。我們來看看用法

          function CardBody() {
          // count 一旦變化后,本組件觸發(fā) rerender
          // 這里如果嫌麻煩,可以使用 lodash 中的 pick 函數(shù)
          const store = BaseStore.useSelector(({ count, increment }) => ({ count, increment }));

          return <div onClick={store.increment}>Text {store.count}div>;
          }

          值得注意的是,createContainer 函數(shù)中返回出去的值不能是每次 render 都重新生成的。我們來修改一下 BaseStore

          export const BaseStore = createContainer(() => {
          // 定義狀態(tài)
          const [count, setCount] = React.useState(0);

          // 之前定義的兩個 function 替換為 useMethods 包裹,保證 increment 、decrement 函數(shù)引用不變
          const methods = useMethods({
          increment() {
          setCount(count + 1);
          },
          decrement() {
          setCount(count - 1);
          },
          });

          return {
          count,
          setCount,
          ...methods,
          };
          });

          這里的 useMethods Hook 就是我之前有篇文章分析過的,用來代替 useCallback,源碼見 Heo。

          錦上添花,可以將 useSelector 結合 lodash.picker 封裝一個更常用的 API,取個名字叫 usePicker

          // createContainer 函數(shù)中

          function usePicker<Selected extends keyof Value>(selected: Selected[]): Pick<Value, Selected> {
          return useSelector((state) => pick(state as Required, selected));
          }

          試試效果:

          function CardBody() {
          const store = BaseStore.usePicker(['count', 'increment']);

          return <div onClick={store.increment}>Text {store.count}div>;
          }

          總結

          好了,這就是我當時寫一個狀態(tài)管理的思路,你學會了嗎?源碼見 Heo,Github 搜 Heo。也是我們正在用的狀態(tài)管理,它足夠輕量、配合 Hooks、完美支持 TS、改造原有代碼的難度小。目前已在生產(chǎn)環(huán)境中穩(wěn)定運行了一年多了,最高復雜度的項目為一次性渲染 2000 多個遞歸結構的組件,性能依然保持得很優(yōu)秀。歡迎大家 Star。

          瀏覽 24
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  欧美在线观看不卡 | 黄色强奸免费小视频网站 | 日本熟妇一区 | A片网电影在线观看 | 大香蕉网站免费 |