1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        教你寫(xiě)一個(gè) React 狀態(tài)管理庫(kù)

        共 8020字,需瀏覽 17分鐘

         ·

        2021-11-15 20:01

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

        通過(guò)對(duì) Redux 的優(yōu)缺點(diǎn)總結(jié)來(lái)看,我們可以自己寫(xiě)一個(gè)狀態(tài)管理庫(kù),本次需要達(dá)到的目的:

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

        因此,閱讀此文檔的前提要對(duì) React Hooks 、typescript 等有一定的概念。OK, 那我們開(kāi)始吧。

        思路

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

        這是三個(gè)最簡(jiǎn)單的函數(shù)式組件 Demo,我們用它來(lái)試驗(yàn):

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

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

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

        實(shí)現(xiàn)

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

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

        // 創(chuàng)建一個(gè) Context,無(wú)需默認(rèn)值,這里為了演示方便,用了斷言
        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>

        );
        }

        接下來(lái)我們?cè)?CardBody 中使用這個(gè) Context,使其穿透取值

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

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

        這樣,一個(gè)最簡(jiǎn)單的穿透狀態(tài)管理的代碼寫(xiě)好了。發(fā)現(xiàn)問(wèn)題了嗎,狀態(tài)的業(yè)務(wù)邏輯寫(xiě)在了 App 組件里,這個(gè)代碼耦合度太高了!我們來(lái)整理一下,需要將 App 的狀態(tài)通過(guò)自定義 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 中使用這個(gè) hook

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

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

        );
        }

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

        useStoreStoreContext.Provider 抽離成一個(gè)組件

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

        再來(lái)看 App 組件,是不是很清晰?

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

        );
        }

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

        // 將自定義 Hook 通過(guò)參數(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,一個(gè)簡(jiǎn)單的狀態(tài)管理算成型了。好不好用我們來(lá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 導(dǎo)出的 Provider

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

        );
        }

        CardBody 使用 BaseStore 導(dǎo)出的 Context,因?yàn)槎x的時(shí)候用了泛型,這里能完美識(shí)別當(dāng)前 store 的形狀,從而具備編輯器智能提示

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

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

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

        調(diào)整

        但是方便和性能總是有所取舍的,毫無(wú)疑問(wèn),成也 Context,敗也 Context。因?yàn)樗拇┩笩o(wú)差別更新特性也就決定了會(huì)讓所有的 React.memo 優(yōu)化失效。一次 setState 幾乎讓整個(gè)項(xiàng)目跟著 rerender ,這是極為不可接受的。因?yàn)樽远x Hook 每次執(zhí)行返回的都是一個(gè)全新的對(duì)象,那么 Provider 每次都會(huì)接受到這個(gè)全新的對(duì)象。所有用到這個(gè) Context 的子組件都跟著一起更新,造成無(wú)意義的損耗調(diào)用。

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

        // createContainer 函數(shù)中

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

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

        聲明一個(gè) Context,用來(lái)放這些子組件的 listener

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

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

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

        函數(shù)類型可以這樣定義:通過(guò)傳入一個(gè)函數(shù),來(lái)手動(dòng)指定需要監(jiān)聽(tīng)的值,并返回這個(gè)值

        // createContainer 函數(shù)中

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

        那我們來(lái)實(shí)現(xiàn)這個(gè) useSelector 。首先是觸發(fā) rerender 的方法,這里用 reducer 讓其內(nèi)部自增,調(diào)用時(shí)不用傳參數(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);

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

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

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

        ref.current = StoreValue;

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

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

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

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

        完整實(shí)現(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。最后一步,我們來(lái)改寫(xiě) 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 就會(huì)讓本組件更新,使 listeners 觸發(fā)調(diào)用,從而使改變狀態(tài)的子組件 render
        listeners.forEach((listener) => {
        listener(value);
        });
        return (
        <Context.Provider value={value}>
        <ListenerContext.Provider value={listeners}>{children}ListenerContext.Provider>

        Context.Provider>
        );
        };

        大功告成!useSelector 返回的新對(duì)象都會(huì)如同 React.memo 一樣做淺對(duì)比。API 用法也如同 React-Redux,毫無(wú)學(xué)習(xí)成本。我們來(lái)看看用法

        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 都重新生成的。我們來(lái)修改一下 BaseStore

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

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

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

        這里的 useMethods Hook 就是我之前有篇文章分析過(guò)的,用來(lái)代替 useCallback,源碼見(jiàn) Heo。

        錦上添花,可以將 useSelector 結(jié)合 lodash.picker 封裝一個(gè)更常用的 API,取個(gè)名字叫 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>;
        }

        總結(jié)

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

        瀏覽 24
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国内成人精品 | 国产成人99久久亚洲综合精品 | 国产精品久久久久久久久久乐趣播 | 大色屌 | 在线免费日韩视频 | 日本乱伦免费视频 | 日本日韩中文字幕波多野吉衣 | 91精品久久久久久久99蜜桃 | 欧美插逼逼 | 洗手间大尺度抚胸激情 |