欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-03-18 21:17:16
目录实现基于 react hooks 的状态共享使用感受本文主要介绍一种基于 react hooks 的状态共享方案,介绍其实现,并总结一下使用感受,目的是在状态管理方面提供多一种选择方式。实现基于...

本文主要介绍一种基于 react hooks 的状态共享方案,介绍其实现,并总结一下使用感受,目的是在状态管理方面提供多一种选择方式。

实现基于 react hooks 的状态共享

react 组件间的状态共享,是一个老生常谈的问题,也有很多解决方案,例如 redux、mobx 等。这些方案很专业,也经历了时间的考验,但私以为他们不太适合一些不算复杂的项目,反而会引入一些额外的复杂度。

实际上很多时候,我不想定义 mutation 和 action、我不想套一层 context,更不想写 connect 和 mapstatetoprops;我想要的是一种轻量、简单的状态共享方案,简简单单引用、简简单单使用。

随着 hooks 的诞生、流行,我的想法得以如愿。

接着介绍一下我目前在用的方案,将 hooks 与发布/订阅模式结合,就能实现一种简单、实用的状态共享方案。因为代码不多,下面将给出完整的实现。

import {
  dispatch,
  setstateaction,
  usecallback,
  useeffect,
  usereducer,
  useref,
  usestate,
} from 'react';

/**
 * @see https://github.com/facebook/react/blob/bb88ce95a87934a655ef842af776c164391131ac/packages/shared/objectis.js
 * inlined object.is polyfill to avoid requiring consumers ship their own
 * https://developer.mozilla.org/en-us/docs/web/javascript/reference/global_objects/object/is
 */
function is(x: any, y: any): boolean {
  return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
}

const objectis = typeof object.is === 'function' ? object.is : is;

/**
 * @see https://github.com/facebook/react/blob/933880b4544a83ce54c8a47f348effe725a58843/packages/shared/shallowequal.js
 * performs equality by iterating through keys on an object and returning false
 * when any key has values which are not strictly equal between the arguments.
 * returns true when the values of all keys are strictly equal.
 */
function shallowequal(obja: any, objb: any): boolean {
  if (is(obja, objb)) {
    return true;
  }

  if (
    typeof obja !== 'object' ||
    obja === null ||
    typeof objb !== 'object' ||
    objb === null
  ) {
    return false;
  }

  const keysa = object.keys(obja);
  const keysb = object.keys(objb);

  if (keysa.length !== keysb.length) {
    return false;
  }

  // test for a's keys different from b.
  for (let i = 0; i < keysa.length; i++) {
    if (
      !object.prototype.hasownproperty.call(objb, keysa[i]) ||
      !is(obja[keysa[i]], objb[keysa[i]])
    ) {
      return false;
    }
  }

  return true;
}

const useforceupdate = () => usereducer(() => ({}), {})[1] as voidfunction;

type isubscriber<t> = (prevstate: t, nextstate: t) => void;

export interface isharedstate<t> {
  /** 静态方式获取数据, 适合在非组件中或者数据无绑定视图的情况下使用 */
  get: () => t;
  /** 修改数据,赋予新值 */
  set: dispatch<setstateaction<t>>;
  /** (浅)合并更新数据 */
  update: dispatch<partial<t>>;
  /** hooks方式获取数据, 适合在组件中使用, 数据变更时会自动重渲染该组件 */
  use: () => t;
  /** 订阅数据的变更 */
  subscribe: (cb: isubscriber<t>) => () => void;
  /** 取消订阅数据的变更 */
  unsubscribe: (cb: isubscriber<t>) => void;
  /** 筛出部分 state */
  usepick<r>(picker: (state: t) => r, deps?: readonly any[]): r;
}

export type ireadonlystate<t> = omit<isharedstate<t>, 'set' | 'update'>;

/**
 * 创建不同实例之间可以共享的状态
 * @param initialstate 初始数据
 */
export const createsharedstate = <t>(initialstate: t): isharedstate<t> => {
  let state = initialstate;
  const subscribers: isubscriber<t>[] = [];

  // 订阅 state 的变化
  const subscribe = (subscriber: isubscriber<t>) => {
    subscribers.push(subscriber);
    return () => unsubscribe(subscriber);
  };

  // 取消订阅 state 的变化
  const unsubscribe = (subscriber: isubscriber<t>) => {
    const index = subscribers.indexof(subscriber);
    index > -1 && subscribers.splice(index, 1);
  };

  // 获取当前最新的 state
  const get = () => state;

  // 变更 state
  const set = (next: setstateaction<t>) => {
    const prevstate = state;
    // @ts-ignore
    const nextstate = typeof next === 'function' ? next(prevstate) : next;
    if (objectis(state, nextstate)) {
      return;
    }
    state = nextstate;
    subscribers.foreach((cb) => cb(prevstate, state));
  };

  // 获取当前最新的 state 的 hooks 用法
  const use = () => {
    const forceupdate = useforceupdate();

    useeffect(() => {
      let ismounted = true;
      // 组件挂载后立即更新一次, 避免无法使用到第一次更新数据
      forceupdate();
      const un = subscribe(() => {
        if (!ismounted) return;
        forceupdate();
      });
      return () => {
        un();
        ismounted = false;
      };
    }, []);

    return state;
  };

  const usepick = <r>(picker: (s: t) => r, deps = []) => {
    const ref = useref<any>({});

    ref.current.picker = picker;

    const [pickedstate, setpickedstate] = usestate<r>(() =>
      ref.current.picker(state),
    );

    ref.current.oldstate = pickedstate;

    const sub = usecallback(() => {
      const pickedold = ref.current.oldstate;
      const pickednew = ref.current.picker(state);
      if (!shallowequal(pickedold, pickednew)) {
        // 避免 pickednew 是一个 function
        setpickedstate(() => pickednew);
      }
    }, []);

    useeffect(() => {
      const un = subscribe(sub);
      return un;
    }, []);

    useeffect(() => {
      sub();
    }, [...deps]);

    return pickedstate;
  };

  return {
    get,
    set,
    update: (input: partial<t>) => {
      set((pre) => ({
        ...pre,
        ...input,
      }));
    },
    use,
    subscribe,
    unsubscribe,
    usepick,
  };
};

拥有 createsharedstate 之后,下一步就能轻易地创建出一个可共享的状态了,在组件中使用的方式也很直接。

// 创建一个状态实例
const countstate = createsharedstate(0);

const a = () => {
  // 在组件中使用 hooks 方式获取响应式数据
  const count = countstate.use();
  return <div>a: {count}</div>;
};

const b = () => {
  // 使用 set 方法修改数据
  return <button onclick={() => countstate.set(count + 1)}>add</button>;
};

const c = () => {
  return (
    <button
      onclick={() => {
        // 使用 get 方法获取数据
        console.log(countstate.get());
      }}
    >
      get
    </button>
  );
};

const app = () => {
  return (
    <>
      <a />
      <b />
      <c />
    </>
  );
};

对于复杂对象,还提供了一种方式,用于在组件中监听指定部分的数据变化,避免其他字段变更造成多余的 render:

const complexstate = createsharedstate({
  a: 0,
  b: {
    c: 0,
  },
});

const a = () => {
  const a = complexstate.usepick((state) => state.a);
  return <div>a: {a}</div>;
};

但复杂对象一般更建议使用组合派生的方式,由多个简单的状态派生出一个复杂的对象。另外在有些时候,我们会需要一种基于原数据的计算结果,所以这里同时提供了一种派生数据的方式。

通过显示声明依赖的方式监听数据源,再传入计算函数,那么就能得到一个响应式的派生结果了。

/**
 * 状态派生(或 computed)
 * ```ts
 * const count1 = createsharedstate(1);
 * const count2 = createsharedstate(2);
 * const count3 = createderivedstate([count1, count2], ([n1, n2]) => n1 + n2);
 * ```
 * @param stores
 * @param fn
 * @param initialvalue
 * @returns
 */
export function createderivedstate<t = any>(
  stores: ireadonlystate<any>[],
  fn: (values: any[]) => t,
  opts?: {
    /**
     * 是否同步响应
     * @default false
     */
    sync?: boolean;
  },
): ireadonlystate<t> & {
  stop: () => void;
} {
  const { sync } = { sync: false, ...opts };
  let values: any[] = stores.map((it) => it.get());
  const innermodel = createsharedstate<t>(fn(values));

  let promise: promise<void> | null = null;

  const uns = stores.map((it, i) => {
    return it.subscribe((_old, newvalue) => {
      values[i] = newvalue;

      if (sync) {
        innermodel.set(() => fn(values));
        return;
      }

      // 异步更新
      promise =
        promise ||
        promise.resolve().then(() => {
          innermodel.set(() => fn(values));
          promise = null;
        });
    });
  });

  return {
    get: innermodel.get,
    use: innermodel.use,
    subscribe: innermodel.subscribe,
    unsubscribe: innermodel.unsubscribe,
    usepick: innermodel.usepick,
    stop: () => {
      uns.foreach((un) => un());
    },
  };
}

至此,基于 hooks 的状态共享方的实现介绍就结束了。

在最近的项目中,有需要状态共享的场景,我都选择了上述方式,在 web 项目和小程序 taro 项目中均能使用同一套实现,一直都比较顺利。

使用感受

最后总结一下目前这种方式的几个特点:

1.实现简单,不引入其他概念,仅在 hooks 的基础上结合发布/订阅模式,类 react 的场景都能使用,比如 taro;

2.使用简单,因为没有其他概念,直接调用 create 方法即可得到 state 的引用,调用 state 实例上的 use 方法即完成了组件和数据的绑定;

3.类型友好,创建 state 时无需定义多余的类型,使用的时候也能较好地自动推导出类型;

4.避免了 hooks 的“闭包陷阱”,因为 state 的引用是恒定的,通过 state 的 get 方法总是能获取到最新的值:

const countstate = createsharedstate(0);

const app = () => {
  useeffect(() => {
    setinterval(() => {
      console.log(countstate.get());
    }, 1000);
  }, []);
  // return ...
};

5.直接支持在多个 react 应用之间共享,在使用一些弹框的时候是比较容易出现多个 react 应用的场景:

const countstate = createsharedstate(0);

const content = () => {
  const count = countstate.use();
  return <div>{count}</div>;
};

const a = () => (
  <button
    onclick={() => {
      dialog.info({
        title: 'alert',
        content: <content />,
      });
    }}
  >
    open
  </button>
);

6.支持在组件外的场景获取/更新数据

7.在 ssr 的场景有较大局限性:state 是细碎、分散创建的,而且 state 的生命周期不是跟随 react 应用,导致无法用同构的方式编写 ssr 应用代码

以上,便是本文的全部内容,实际上 hooks 到目前流行了这么久,社区当中已有不少新型的状态共享实现方式,这里仅作为一种参考。

根据以上特点,这种方式有明显的优点,也有致命的缺陷(对于 ssr 而言),但在实际使用中,可以根据具体的情况来选择合适的方式。比如在 taro2 的小程序应用中,无需关心 ssr,那么我更倾向于这种方式;如果在 ssr 的同构项目中,那么定还是老老实实选择 redux。

总之,是多了一种选择,到底怎么用还得视具体情况而定。 

以上就是基于react hooks的小型状态管理详解的详细内容,更多关于react hooks 小型状态管理的资料请关注其它相关文章!