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

2022-02-17 10:52:17 +08:00
 nanxiaobei

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

1804 次点击
所在节点    React
2 条回复
lizhenda
2022-02-17 22:54:01 +08:00
全部拜读了一遍,很不错的思考
aaronlam
2022-02-18 02:43:23 +08:00
整个库用法的确是很简单,很认同楼主的这种设计 API 的方式。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/834435

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX