1. resso,React 状态管理从未如此简单
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);
}
};
其它组件同理,订阅新的 setA
到 listeners
中,即可实现组件的 "按需更新"。
以上功能也可以利用 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 的写法,主要有以下考虑:
- 需深层遍历所有数据进行 proxy,且更新数据时也需要先 proxy 化,会有一定的性能损耗。(resso 只在初始化时 proxy store 一次。)
- 因为所有数据都是 proxy,在 Chrome console 打印时显示不友好,这是很大的问题。(resso 不会有这个问题,因为只有 store 是 proxy,而一般是打印 store 内的数据。)
- 若解构出子数据,例如
obj
,obj.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