resso,世界上最簡單的 React 狀態管理器

南小北發表於2022-02-18

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);
  }
};

其它元件同理,訂閱新的 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

相關文章