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