耀极客论坛

 找回密码
 立即注册
查看: 700|回复: 0

基于React Hooks的小型状态管理详解

[复制链接]

336

主题

318

帖子

22万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
220555
发表于 2022-5-8 01:48:42 | 显示全部楼层 |阅读模式
  本文主要介绍一种基于 React Hooks 的状态共享方案,介绍其实现,并总结一下使用感受,目的是在状态管理方面提供多一种选择方式。感兴趣的小伙伴可以了解一下
  本文主要介绍一种基于 React Hooks 的状态共享方案,介绍其实现,并总结一下使用感受,目的是在状态管理方面提供多一种选择方式。

实现基于 React Hooks 的状态共享
  React 组件间的状态共享,是一个老生常谈的问题,也有很多解决方案,例如 Redux、MobX 等。这些方案很专业,也经历了时间的考验,但私以为他们不太适合一些不算复杂的项目,反而会引入一些额外的复杂度。
  实际上很多时候,我不想定义 mutation 和 action、我不想套一层 context,更不想写 connect 和 mapStateToProps;我想要的是一种轻量、简单的状态共享方案,简简单单引用、简简单单使用。
  随着 Hooks 的诞生、流行,我的想法得以如愿。
  接着介绍一下我目前在用的方案,将 Hooks 与发布/订阅模式结合,就能实现一种简单、实用的状态共享方案。因为代码不多,下面将给出完整的实现。
  1. import {
  2.   Dispatch,
  3.   SetStateAction,
  4.   useCallback,
  5.   useEffect,
  6.   useReducer,
  7.   useRef,
  8.   useState,
  9. } from 'react';
  10. /**
  11. * @see https://github.com/facebook/react/blob/bb88ce95a87934a655ef842af776c164391131ac/packages/shared/objectIs.js
  12. * inlined Object.is polyfill to avoid requiring consumers ship their own
  13. * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
  14. */
  15. function is(x: any, y: any): boolean {
  16.   return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
  17. }
  18. const objectIs = typeof Object.is === 'function' ? Object.is : is;
  19. /**
  20. * @see https://github.com/facebook/react/blob/933880b4544a83ce54c8a47f348effe725a58843/packages/shared/shallowEqual.js
  21. * Performs equality by iterating through keys on an object and returning false
  22. * when any key has values which are not strictly equal between the arguments.
  23. * Returns true when the values of all keys are strictly equal.
  24. */
  25. function shallowEqual(objA: any, objB: any): boolean {
  26.   if (is(objA, objB)) {
  27.     return true;
  28.   }
  29.   if (
  30.     typeof objA !== 'object' ||
  31.     objA === null ||
  32.     typeof objB !== 'object' ||
  33.     objB === null
  34.   ) {
  35.     return false;
  36.   }
  37.   const keysA = Object.keys(objA);
  38.   const keysB = Object.keys(objB);
  39.   if (keysA.length !== keysB.length) {
  40.     return false;
  41.   }
  42.   // Test for A's keys different from B.
  43.   for (let i = 0; i ‹ keysA.length; i++) {
  44.     if (
  45.       !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
  46.       !is(objA[keysA[i]], objB[keysA[i]])
  47.     ) {
  48.       return false;
  49.     }
  50.   }
  51.   return true;
  52. }
  53. const useForceUpdate = () => useReducer(() => ({}), {})[1] as VoidFunction;
  54. type ISubscriber‹T> = (prevState: T, nextState: T) => void;
  55. export interface ISharedState‹T> {
  56.   /** 静态方式获取数据, 适合在非组件中或者数据无绑定视图的情况下使用 */
  57.   get: () => T;
  58.   /** 修改数据,赋予新值 */
  59.   set: Dispatch‹SetStateAction‹T>>;
  60.   /** (浅)合并更新数据 */
  61.   update: Dispatch‹Partial‹T>>;
  62.   /** hooks方式获取数据, 适合在组件中使用, 数据变更时会自动重渲染该组件 */
  63.   use: () => T;
  64.   /** 订阅数据的变更 */
  65.   subscribe: (cb: ISubscriber‹T>) => () => void;
  66.   /** 取消订阅数据的变更 */
  67.   unsubscribe: (cb: ISubscriber‹T>) => void;
  68.   /** 筛出部分 state */
  69.   usePick‹R>(picker: (state: T) => R, deps?: readonly any[]): R;
  70. }
  71. export type IReadonlyState‹T> = Omit‹ISharedState‹T>, 'set' | 'update'>;
  72. /**
  73. * 创建不同实例之间可以共享的状态
  74. * @param initialState 初始数据
  75. */
  76. export const createSharedState = ‹T>(initialState: T): ISharedState‹T> => {
  77.   let state = initialState;
  78.   const subscribers: ISubscriber‹T>[] = [];
  79.   // 订阅 state 的变化
  80.   const subscribe = (subscriber: ISubscriber‹T>) => {
  81.     subscribers.push(subscriber);
  82.     return () => unsubscribe(subscriber);
  83.   };
  84.   // 取消订阅 state 的变化
  85.   const unsubscribe = (subscriber: ISubscriber‹T>) => {
  86.     const index = subscribers.indexOf(subscriber);
  87.     index > -1 && subscribers.splice(index, 1);
  88.   };
  89.   // 获取当前最新的 state
  90.   const get = () => state;
  91.   // 变更 state
  92.   const set = (next: SetStateAction‹T>) => {
  93.     const prevState = state;
  94.     // @ts-ignore
  95.     const nextState = typeof next === 'function' ? next(prevState) : next;
  96.     if (objectIs(state, nextState)) {
  97.       return;
  98.     }
  99.     state = nextState;
  100.     subscribers.forEach((cb) => cb(prevState, state));
  101.   };
  102.   // 获取当前最新的 state 的 hooks 用法
  103.   const use = () => {
  104.     const forceUpdate = useForceUpdate();
  105.     useEffect(() => {
  106.       let isMounted = true;
  107.       // 组件挂载后立即更新一次, 避免无法使用到第一次更新数据
  108.       forceUpdate();
  109.       const un = subscribe(() => {
  110.         if (!isMounted) return;
  111.         forceUpdate();
  112.       });
  113.       return () => {
  114.         un();
  115.         isMounted = false;
  116.       };
  117.     }, []);
  118.     return state;
  119.   };
  120.   const usePick = ‹R>(picker: (s: T) => R, deps = []) => {
  121.     const ref = useRef‹any>({});
  122.     ref.current.picker = picker;
  123.     const [pickedState, setPickedState] = useState‹R>(() =>
  124.       ref.current.picker(state),
  125.     );
  126.     ref.current.oldState = pickedState;
  127.     const sub = useCallback(() => {
  128.       const pickedOld = ref.current.oldState;
  129.       const pickedNew = ref.current.picker(state);
  130.       if (!shallowEqual(pickedOld, pickedNew)) {
  131.         // 避免 pickedNew 是一个 function
  132.         setPickedState(() => pickedNew);
  133.       }
  134.     }, []);
  135.     useEffect(() => {
  136.       const un = subscribe(sub);
  137.       return un;
  138.     }, []);
  139.     useEffect(() => {
  140.       sub();
  141.     }, [...deps]);
  142.     return pickedState;
  143.   };
  144.   return {
  145.     get,
  146.     set,
  147.     update: (input: Partial‹T>) => {
  148.       set((pre) => ({
  149.         ...pre,
  150.         ...input,
  151.       }));
  152.     },
  153.     use,
  154.     subscribe,
  155.     unsubscribe,
  156.     usePick,
  157.   };
  158. };
复制代码
  拥有 createSharedState 之后,下一步就能轻易地创建出一个可共享的状态了,在组件中使用的方式也很直接。
  1. // 创建一个状态实例
  2. const countState = createSharedState(0);
  3. const A = () => {
  4.   // 在组件中使用 hooks 方式获取响应式数据
  5.   const count = countState.use();
  6.   return ‹div>A: {count}‹/div>;
  7. };
  8. const B = () => {
  9.   // 使用 set 方法修改数据
  10.   return ‹button onClick={() => countState.set(count + 1)}>Add‹/button>;
  11. };
  12. const C = () => {
  13.   return (
  14.     ‹button
  15.       onClick={() => {
  16.         // 使用 get 方法获取数据
  17.         console.log(countState.get());
  18.       }}
  19.     >
  20.       Get
  21.     ‹/button>
  22.   );
  23. };
  24. const App = () => {
  25.   return (
  26.     ‹>
  27.       ‹A />
  28.       ‹B />
  29.       ‹C />
  30.     ‹/>
  31.   );
  32. };
复制代码
  对于复杂对象,还提供了一种方式,用于在组件中监听指定部分的数据变化,避免其他字段变更造成多余的 render:
  1. const complexState = createSharedState({
  2.   a: 0,
  3.   b: {
  4.     c: 0,
  5.   },
  6. });
  7. const A = () => {
  8.   const a = complexState.usePick((state) => state.a);
  9.   return ‹div>A: {a}‹/div>;
  10. };
复制代码
  但复杂对象一般更建议使用组合派生的方式,由多个简单的状态派生出一个复杂的对象。另外在有些时候,我们会需要一种基于原数据的计算结果,所以这里同时提供了一种派生数据的方式。
  通过显示声明依赖的方式监听数据源,再传入计算函数,那么就能得到一个响应式的派生结果了。
  1. /**
  2. * 状态派生(或 computed)
  3. * ```ts
  4. * const count1 = createSharedState(1);
  5. * const count2 = createSharedState(2);
  6. * const count3 = createDerivedState([count1, count2], ([n1, n2]) => n1 + n2);
  7. * ```
  8. * @param stores
  9. * @param fn
  10. * @param initialValue
  11. * @returns
  12. */
  13. export function createDerivedState‹T = any>(
  14.   stores: IReadonlyState‹any>[],
  15.   fn: (values: any[]) => T,
  16.   opts?: {
  17.     /**
  18.      * 是否同步响应
  19.      * @default false
  20.      */
  21.     sync?: boolean;
  22.   },
  23. ): IReadonlyState‹T> & {
  24.   stop: () => void;
  25. } {
  26.   const { sync } = { sync: false, ...opts };
  27.   let values: any[] = stores.map((it) => it.get());
  28.   const innerModel = createSharedState‹T>(fn(values));
  29.   let promise: Promise‹void> | null = null;
  30.   const uns = stores.map((it, i) => {
  31.     return it.subscribe((_old, newValue) => {
  32.       values[i] = newValue;
  33.       if (sync) {
  34.         innerModel.set(() => fn(values));
  35.         return;
  36.       }
  37.       // 异步更新
  38.       promise =
  39.         promise ||
  40.         Promise.resolve().then(() => {
  41.           innerModel.set(() => fn(values));
  42.           promise = null;
  43.         });
  44.     });
  45.   });
  46.   return {
  47.     get: innerModel.get,
  48.     use: innerModel.use,
  49.     subscribe: innerModel.subscribe,
  50.     unsubscribe: innerModel.unsubscribe,
  51.     usePick: innerModel.usePick,
  52.     stop: () => {
  53.       uns.forEach((un) => un());
  54.     },
  55.   };
  56. }
复制代码
  至此,基于 Hooks 的状态共享方的实现介绍就结束了。
  在最近的项目中,有需要状态共享的场景,我都选择了上述方式,在 Web 项目和小程序 Taro 项目中均能使用同一套实现,一直都比较顺利。

使用感受
  最后总结一下目前这种方式的几个特点:
  1.实现简单,不引入其他概念,仅在 Hooks 的基础上结合发布/订阅模式,类 React 的场景都能使用,比如 Taro;
  2.使用简单,因为没有其他概念,直接调用 create 方法即可得到 state 的引用,调用 state 实例上的 use 方法即完成了组件和数据的绑定;
  3.类型友好,创建 state 时无需定义多余的类型,使用的时候也能较好地自动推导出类型;
  4.避免了 Hooks 的“闭包陷阱”,因为 state 的引用是恒定的,通过 state 的 get 方法总是能获取到最新的值:
  1. const countState = createSharedState(0);
  2. const App = () => {
  3.   useEffect(() => {
  4.     setInterval(() => {
  5.       console.log(countState.get());
  6.     }, 1000);
  7.   }, []);
  8.   // return ...
  9. };
复制代码
  5.直接支持在多个 React 应用之间共享,在使用一些弹框的时候是比较容易出现多个 React 应用的场景:
  1. const countState = createSharedState(0);
  2. const Content = () => {
  3.   const count = countState.use();
  4.   return ‹div>{count}‹/div>;
  5. };
  6. const A = () => (
  7.   ‹button
  8.     onClick={() => {
  9.       Dialog.info({
  10.         title: 'Alert',
  11.         content: ‹Content />,
  12.       });
  13.     }}
  14.   >
  15.     open
  16.   ‹/button>
  17. );
复制代码
  6.支持在组件外的场景获取/更新数据
  7.在 SSR 的场景有较大局限性:state 是细碎、分散创建的,而且 state 的生命周期不是跟随 React 应用,导致无法用同构的方式编写 SSR 应用代码
  以上,便是本文的全部内容,实际上 Hooks 到目前流行了这么久,社区当中已有不少新型的状态共享实现方式,这里仅作为一种参考。
  根据以上特点,这种方式有明显的优点,也有致命的缺陷(对于 SSR 而言),但在实际使用中,可以根据具体的情况来选择合适的方式。比如在 Taro2 的小程序应用中,无需关心 SSR,那么我更倾向于这种方式;如果在 SSR 的同构项目中,那么定还是老老实实选择 Redux。
  总之,是多了一种选择,到底怎么用还得视具体情况而定。 
  以上就是基于React Hooks的小型状态管理详解的详细内容,更多关于React Hooks 小型状态管理的资料请关注脚本之家其它相关文章!


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|耀极客论坛 ( 粤ICP备2022052845号-2 )|网站地图

GMT+8, 2022-12-7 10:39 , Processed in 0.073805 second(s), 20 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表