resso,设计一个最简单的 React 状态管理器 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
nanxiaobei
V2EX    React

resso,设计一个最简单的 React 状态管理器

  •  
  •   nanxiaobei
    nanxiaobei 2022-02-17 10:52:17 +08:00 2438 次点击
    这是一个创建于 1408 天前的主题,其中的信息可能已经有所发展或是发生改变。

    1. resso ,世界上最简单的状态管理器

    resso 是一个全新的 React 状态管理器,它的目的是提供世界上最简单的使用方式。

    同时,resso 还实现了按需更新,组件未用到的数据有变化,绝不触发组件更新。

    GitHub: https://github.com/nanxiaobei/resso

    import resso from 'resso'; const store = resso({ count: 0, text: 'hello' }); function App() { const { count } = store; // 先解构,再使用 return ( <> {count} <button OnClick={() => store.count++}>+</button> </> ); } 

    只有一个 API resso,包裹一下 store 对象就行,再没别的了。

    如需更新,对 store 的 key 重新赋值即可。

    2. React 状态管理器是如何工作的?

    假设有一个 store ,注入到在不同的组件中:

    let store = { count: 0, text: 'hello', }; // Component A const { count } = store; const [, setA] = useState(); // Component B const { text } = store; const [, setB] = useState(); // Component C const { text } = store; const [, setC] = useState(); // 初始化 const listeners = [setA, setB, setC]; // 更新 store = { ...store, count: 1 }; listeners.forEach((setState) => setState(store)); 

    将各个组件的 setState 放到一个数组中,更新 store 时,把 listeners 都调用一遍,这样就可以触发所有组件的更新。

    如何监听 store 数据变化呢?可以提供一个公共更新函数(例如 Redux 的 dispatch),若调用即为更新。也可以利用 proxy 的 setter 来监听。

    是的,几乎所有的状态管理器都是这么工作的,就是这么简单。比如 Redux 的源码:https://github.com/reduxjs/redux/blob/master/src/createStore.ts#L265-L268

    3. 如何优化更新性能?

    每次更新 store 都会调用 listeners 中所有的 setState ,这会导致性能问题。

    例如更新 count 时,理论上只希望 A 更新,而此时 B 和 C 也跟着更新了,但它们根本没用到 count

    如何按需更新呢?可以使用 selector 的方式(例如 Redux 的 useSelector,或者 zustand 的实现):

    // Component A const { count } = store; const [, rawSetA] = useState(); const selector = (store) => store.count; const setA = (newStore) => { if (count !== selector(newStore)) { rawSetA(newStore); } }; 

    其它组件同理,订阅新的 setAlisteners 中,即可实现组件的 "按需更新"。

    以上功能也可以利用 proxy 的 getter 来实现,通过 getter 来知晓组件 "用到" 的数据。

    4. resso 内部如何实现的?

    上面的实现中,是在每个组件中收集一个 setState 。更新 store 时,通过数据比对,确定是否更新组件。

    resso 使用了一种新的思路,其实更符合 Hooks 的元数据理念:

    let store = { count: 0, text: 'hello', }; // Component A const [count, setACount] = useState(store.count); // Component B const [text, setBText] = useState(store.text); // Component C const [text, setCText] = useState(store.text); // 初始化 const listenerMap = { count: [setACount], text: [setBText, setCText], }; // 更新 store = { ...store, count: 1 }; listenerMap.count.forEach((setCount) => setCount(store.count)); 

    使用 useState 注入组件中用到的每一个 store 数据,同时维护一个针对 store 中每个 key 的更新列表。

    在每个组件中收集的 setState 数量,与用到的 store 数据一一对应。而非只收集一个 setState 用于组件更新。

    在更新时,就不需要再做数据比对,因为更新单元是基于 "数据" 级别,而非基于 "组件" 级别。

    更新某个数据,就是调用这个数据的更新列表,而非组件的更新列表。将整个 store 元数据化。

    5. resso 的 API 是如何设计的?

    设计 API 的秘诀是:先把最想要的用法写出来,然后再去想实现方式。这样做出来的东西一定是最符合直觉的。

    resso 一开始也想过以下几种 API 设计:

    1. 类似 valtio

    const store = resso({ count: 0, text: 'hello' }); const snap = useStore(store); const { count, text } = snap; // get store.count++; // set 

    这是标准的 Hooks 用法,缺点是得多加一个 API useStore。而且 get 时使用 snap ,set 时使用 store ,让人分裂,这肯定不是 "最简单" 的设计。

    2. 类似 valtio/macro

    const store = resso({ count: 0, text: 'hello' }); useStore(store); const { count, text } = store; // get store.count++; // set 

    这也是可以实现的,而且也是标准的 Hooks 用法。此时统一了 get 和 set 主体,但还是得多加一个 useStore API ,这玩意仅仅是为了调用 Hooks ,如果用户忘了写呢?

    而且实践中发现,在每个组件中使用 store ,都得 import 两个东西,store 和 useStore ,这肯定不如只 import 一个 store 简洁,尤其是用到的地方很多时会很麻烦。

    3. 为了只 import 一个 store

    const store = resso({ count: 0, text: 'hello' }); store.useStore(); const { count, text } = store; // get store.count++; // set 

    这是最后一次 "合法" 使用 Hooks 的希望,只 import 一个 store ,但总归还是看起来很怪,无法接受。

    如果大家试着去设计这个 API ,会发现若想直接更新 store (需要 import store ),又想通过 Hooks 解构出 store 数据(需要多 import 一个 Hook ,同时 get 和 set 不同源),这个设计不管怎么都会看起来很别扭。

    为了终极简洁,为了最简单的使用方式,resso 最终还是踏上了这样的 API 设计:

    const store = resso({ count: 0, text: 'hello' }); const { count } = store; // get store.count++; // set 

    6. resso 的使用方式

    Get store

    因为 store 数据是以 useState 注入组件,所以需要先解构(解构即调用 useState),在组件的最顶层解构(即 Hooks 规则,不能写在 if 后),然后再使用,否则将会有 React warning 。

    Set store

    对 store 的第一层数据赋值,将触发更新,且仅对第一层数据的赋值触发更新。

    store.obj = { ...store.obj, num: 10 }; // 触发更新 store.obj.num = 10; // 不触发更新(请注意 valtio 支持这种写法) 

    resso 未支持 valtio 的写法,主要有以下考虑:

    1. 需深层遍历所有数据进行 proxy ,且更新数据时也需要先 proxy 化,会有一定的性能损耗。( resso 只在初始化时 proxy store 一次。)
    2. 因为所有数据都是 proxy ,在 Chrome console 打印时显示不友好,这是很大的问题。( resso 不会有这个问题,因为只有 store 是 proxy ,而一般是打印 store 内的数据。)
    3. 若解构出子数据,例如 objobj.num = 10 也可以触发更新,会造成数据来源不透明,是否来自 store 、赋值是否触发更新不确定。( resso 更新的主体永远是 store ,来源清晰。)

    7. Make simple, not chaos

    以上即是 resso 的设计理念,以及 React 状态管理器的一些实现方式。

    归根结底,React 状态管理器是工具,React 是工具,JS 是工具,编程是工具,工作本身也是工具。

    工具的目的,是为了创造,创造出作用于现实世界的作品,而非工具本身。

    所以,为什么不简单一些呢?

    jQuery 是为了简化原生 JS 的开发,React 是为了简化 jQuery 的开发,开发是为了简化现实世界的流程,互联网是为了简化人们的沟通路径、工作路径、消费路径,开发的意义是简化,互联网的意义是简化,互联网的价值也在于简化。

    所以,为什么不简单一些呢?

    Chic. Not geek.

    简单即是一切。

    try try resso: https://github.com/nanxiaobei/resso

    2 条回复    2022-02-18 02:43:23 +08:00
        1
    lizhenda  
       2022-02-17 22:54:01 +08:00
    全部拜读了一遍,很不错的思考
    aaronlam
        2
    aaronlam  
       2022-02-18 02:43:23 +08:00
    整个库用法的确是很简单,很认同楼主的这种设计 API 的方式。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2571 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 14:57 PVG 22:57 LAX 06:57 JFK 09:57
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86