本文主要介绍一种基于 react hooks 的状态共享方案,介绍其实现,并总结一下使用感受,目的是在状态管理方面提供多一种选择方式。
react 组件间的状态共享,是一个老生常谈的问题,也有很多解决方案,例如 redux、mobx 等。这些方案很专业,也经历了时间的考验,但私以为他们不太适合一些不算复杂的项目,反而会引入一些额外的复杂度。
实际上很多时候,我不想定义 mutation 和 action、我不想套一层 context,更不想写 connect 和 mapstatetoprops;我想要的是一种轻量、简单的状态共享方案,简简单单引用、简简单单使用。
随着 hooks 的诞生、流行,我的想法得以如愿。
接着介绍一下我目前在用的方案,将 hooks 与发布/订阅模式结合,就能实现一种简单、实用的状态共享方案。因为代码不多,下面将给出完整的实现。
import { dispatch, tstateaction, ucallback, ueffect, ureducer, uref, ustate,} from 'react';/** * @e 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;/** * @e https://github.com/facebook/react/blob/933880b4544a83ce54c8a47f348effe725a58843/packages/shared/shallowequal.js * performs equality by iterating through keys on an object and returning fal * 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 fal; } const keysa = object.keys(obja); const keysb = object.keys(objb); if (keysa.length !== keysb.length) { return fal; } // 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 fal; } } return true;}const uforceupdate = () => ureducer(() => ({}), {})[1] as voidfunction;type isubscriber<t> = (prevstate: t, nextstate: t) => void;export interface isharedstate<t> { /** 静态方式获取数据, 适合在非组件中或者数据无绑定视图的情况下使用 */ get: () => t; /** 修改数据,赋予新值 */ t: dispatch<tstateaction<t>>; /** (浅)合并更新数据 */ update: dispatch<partial<t>>;英语经典语句 /** hooks方式获取数据, 适合在组件中使用, 数据变更时会自动重渲染该组件 */ u: () => t; /** 订阅数据的变更 */ subscribe: (cb: isubscriber<t>) => () => void; /** 取消订阅数据的变更 */ unsubscribe: (cb: isubscriber<t>) => void; /** 筛出部分 state */ upick<r>(picker: (state: t) => r, deps?: readonly any[]): r;}export type ireadonlystate<t> = omit<isharedstate<t>, 't' | '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 t = (next: tstateaction<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 u = () => { const forceupdate = uforceupdate(); ueffect(() => { let ismounted = true; // 组件挂载后立即更新一次, 避免无法使用到第一次更新数据 forceupdate(); const un = subscribe(() => { if (!ismounted) return; forceupdate(); }); return () => { un(); ismounted = fal; }; }, []); return state; }; const upick = <r>(picker: (s: t) => r, deps = []) => { const ref = uref<any>({}); ref.current.picker = picker; const [pickedstate, tpickedstate] = ustate<r>(() => ref.current.picker(state), ); ref.current.oldstate = pickedstate; const sub = ucallback(() => { const pickedold = ref.此曲只应天上有打一成语current.oldstate; const pickednew = ref.current.picker(state); if (!shallowequal(pickedold, pickednew)) { // 避免 pickednew 是一个 function tpickedstate(() => pickednew); } }, []); ueffect(() => { const un = subscribe(sub); return un; }, []); ueffect(() => { sub(); }, [...deps]); return pickedstate; }; return { get, t, update: (input: partial<t>) => { t((pre) => ({ ...pre, ...input, })); }, u, subscribe, unsubscribe, upick, };};
拥有 createsharedstate 之后,下一步就能轻易地创建出一个可共享的状态了,在组件中使用的方式也很直接。
// 创建一个状态实例const countstate = createsharedstate(0);const a = () => { // 在组件中使用 hooks 方式获取响应式数据 const count = countstate.u(); return <div>a: {count}</div>;};const b = () => { // 使用 t 方法修改数据 return <button onclick={() => countstate.t(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.upick((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 fal */ sync?: boolean; },): ireadonlystate<t> & { stop: () => void;} { const { sync } = { sync: fal, ...opts }; let values: any[] = stores.map((it) => it.get()); const innermodel = createsharedstate<t>(fn(values)); let promi: promi<void> | null = null; const uns = stores.map((it, i) => { return it.subscribe((_old, newvalue) => { values[i] = newvalue; if (sync) { innermodel.t(() => fn(values)); return; } // 异步更新 promi = promi || promi.resolve().then(() => { innermodel.t(() => fn(values)); promi = null; }); }); }); return { get: innermodel.get, u: innermodel.u, subscribe: innermodel.subscribe, unsubscribe: innermodel.unsubscribe, upick: innermodel.upick, stop: () => { uns.foreach((un) => un()); }, };}
至此,基于 hooks 的状态共享方的实现介绍就结束了。
在最近的项目中,有需要状态共享的场景,我都选择了上述方式,在 web 项目和小程序 taro 项目中均能使用同一套实现,一直都比较顺利。
最后总结一下目前这种方式的几个特点:
1.实现简单,不引入其他概念,仅在 hooks 的基础上结合发布/订阅模式,类 react 的场景都能使用,比如 taro;
2.使用简单,因为没有其他概念,直接调用 create 方法即可得到 state 的引用,调用 state 实例上你是我爸爸的 u 方法即完成了组件和数据的绑定;
3.类型友好,创建 state 时无需定义多余的类型,使用的时候也能较好地自动推导出类型;
4.避免了 hooks 的“闭包陷阱”,因为 state 的引用是恒定的,通过 state 的 get 方法总是能获取到最新的值:
const countstate = createsharedstate(0);const app = () => { ueffect(() => { tinterval(() => { console.log(countstate.get()); }, 1000); }, []); // return ...};
5.直接支持在多个 react 应用之间共享,在使用一些弹框的时候是比较容易出现多个 react 应用的场景:
const countstate = createsharedstate(0);const content = () => { const count = countstate.u(); 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 小型状态管理的资料请关注www.887551.com其它相关文章!
本文发布于:2023-04-04 07:43:00,感谢您对本站的认可!
本文链接:https://www.wtabcd.cn/fanwen/zuowen/d71e8d8bf685582da8013522684fc532.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文word下载地址:基于React Hooks的小型状态管理详解.doc
本文 PDF 下载地址:基于React Hooks的小型状态管理详解.pdf
留言与评论(共有 0 条评论) |