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

1,000 阅读6分钟

1. resso,React 状态管理从未如此简单

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

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

GitHub: github.com/nanxiaobei/…

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 [, updateA] = useState();

// Component B
const { text } = store;
const [, updateB] = useState();

// 初始化
const listeners = [updateA, updateB];

// 更新
store = { ...store, count: 1 };
listeners.forEach((update) => update(store));

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

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

是的,几乎所有的状态管理器都是这么工作的,就是这么简单。比如 Redux 的源码:github.com/reduxjs/red…

3. 如何优化更新性能?

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

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

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

// Component A
const { count } = store;
const [, rawUpdateA] = useState();

const selector = (store) => store.count;

const updateA = (newStore) => {
  if (count !== selector(newStore)) rawUpdateA(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 [count, setBCount] = useState(store.count);
const [text, setBText] = useState(store.text);

// 初始化
const listenerMap = {
  count: [setACount, setBCount],
  text: [setBText],
};

// 更新
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. Simple. Not chaos

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

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

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

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

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

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

Chic. Not geek.

E0vcN6EWUAMC_sn.jpeg

简单即是一切。

try try resso: github.com/nanxiaobei/…