保持在一线写前端代码大略有七八年了,写过一些我的项目,有过一些反思,越来越确信素日里始终用得心安理得某些的货色兴许存在着问题,比方:在 状态治理 上始终比拟风行的实际 ,所以试着分享进去探讨一下。
为什么要辞别 Redux、Recoil、MobX、Zustand、Jotai 还有 Valtio
明天风行的状态治理库有很多,尤其在 React 中。为了把问题说得清晰一些,我想以 React 中的几个支流库切入聊起。
首先看一下 Redux。对于单个状态的变动,能够 dispatch 简略 action。想晓得这个简略 action 会扭转什么状态,依据 Redux 的设计,查看它申明在哪个 slice 里就能够了:
const checkboxSlice = createSlice({ name: 'checkbox', initialState: { checked: false, }, reducers: { check(state) { // ... }, },});const { check } = checkboxSlice.actions;// ...dispatch(check());// 因为 `check` 申明在 `checkboxSlice` 里,依据 Redux 的设计能够晓得 `check` 扭转的是 `checkboxSlice` 代表的状态。
而对于多个状态的变动,须要 dispatch 简单 action。想晓得这个简单 action 会扭转什么状态,只查看它申明在哪里是不够的:
const checkboxSlice = createSlice({ name: 'checkbox', initialState: { checked: false, }, reducers: { check(state) { // ... },++ uncheck(state) {+ // ...+ } },});-const { check } = checkboxSlice.actions;+const { check, uncheck } = checkboxSlice.actions;// 先构建简单 action `uncheckWithTextCleaned` 要调用的底层简略 action `uncheck`,而这个简略 action 大概率不会在别的中央用到了。
const textareaSlice = createSlice({ name: 'textarea', initialState: { text: '', }, reducers: { setText(state, action: PayloadAction<string>) { // ... }, },});const { setText } = textareaSlice.actions;function uncheckWithTextCleaned(): AppThunk { return (dispatch) => { // ... };}// ...dispatch(uncheckWithTextCleaned());// 在只查看 `uncheckWithTextCleaned` 的函数申明的状况下,无奈晓得这个简单 action 会扭转什么状态。
如果不追踪函数体,就无奈晓得简单 action 会扭转什么状态,那么状态变动就变得不可预测了。如果追踪了函数体,只管能够晓得会扭转什么状态,但应用上的总体开发老本也就随着减轻了:
function uncheckWithTextCleaned(): AppThunk { return (dispatch) => { dispatch(uncheck()); dispatch(setText('')); };}// ...dispatch(uncheckWithTextCleaned());// 通过追踪函数体发现 `uncheckWithTextCleaned` 调用了 `uncheck` 和 `setText`,因为 `uncheck` 申明在 `checkboxSlice` 里,`setText` 申明在 `textareaSlice`,能够晓得 `uncheckWithTextCleaned` 扭转的是 `checkboxSlice` 和 `textareaSlice` 代表的状态。
此外,在简单 action 要调用的底层简略 action 还没筹备好的时候,就要先构建这些要调用的简略 action,而这些简略 action 大概率不会在别的中央用到了。这样,简单 action 就与底层 slice 高耦合了,会导致开发艰难,也就使老本进一步减轻了。
再看一下 Recoil 和 MobX。在 Recoil 中是通过自定义 hook 来封装状态变动的:
const checkboxState = atom({ key: 'checkbox', default: { checked: false, },});const textareaState = atom({ key: 'textarea', default: { text: '', },});// ...function useSetText() { return useRecoilCallback( ({ set }) => (text: string) => { // ... }, [] );}function useUncheckWithTextCleaned() { const setText = useSetText(); return useRecoilCallback( ({ set }) => () => { // ... }, [] );}// ...const uncheckWithTextCleaned = useUncheckWithTextCleaned();// ...uncheckWithTextCleaned();// 在只查看 `uncheckWithTextCleaned` 或 `useUncheckWithTextCleaned` 的函数申明的状况下,无奈晓得这个 hook 会扭转什么状态。须要通过追踪函数体发现间接或间接发动的 `set` 调用能力晓得会扭转什么状态。
在 MobX 中是通过类的办法来封装状态变动的:
class CheckboxStore { private textareaStore: TextareaStore; checked: boolean; constructor(textareaStore: TextareaStore) { makeAutoObservable(this); this.textareaStore = textareaStore; this.checked = false; } uncheckWithTextCleaned(): void { // ... }}class TextareaStore { text: string; constructor() { makeAutoObservable(this); this.text = ''; } setText(text): void { // ... }}// ...checkboxStore.uncheckWithTextCleaned();// 在只查看 `checkboxStore.uncheckWithTextCleaned` 的函数申明的状况下,无奈晓得这个办法会扭转什么状态。须要通过追踪函数体发现间接或间接扭转的 property 能力晓得会扭转什么状态。
与 Redux 相似地,如果不追踪函数体,状态变动就不可预测了。如果追踪了函数体,应用老本就减轻了。
此外,因为 Recoil 较多地思考了异步状态变动,在自定义 hook 中获取状态会比拟麻烦,因为 MobX 有独立的订阅机制,得当应用须要精确了解。这些,都使老本进一步减轻了。
而余下的三个库,Zustand、Jotai 和 Valtio,它们用起来别离十分像 Redux、Recoil 和 MobX。或者能够说,前几者基本上是后几者的简化版。
小结一下,React 中的支流状态治理库在 (1) 状态变动的可预测性 和 (2) 应用上的总体开发老本 上存在着问题。如果略微看一下其余框架中最支流的状态治理库,会发现它们也有相似问题。所以能够说,这两个问题是普遍存在的。
可预测性与副作用
当函数在输入返回值之外还产生了其余成果,那这个函数就是有副作用(side effect)的。像下面例子中的函数,副作用都是扭转状态。
而函数有副作用不等同于函数行为是不可预测的。只有副作用是可控的,函数行为就是可预测的。像 Redux 例子中的简略 action,依据 Redux 的设计只能扭转申明各自的 slice 所代表的状态。然而,对函数的副作用不加以控制的话,随着函数体的复杂度回升副作用的可控性就会降落,最终,不可控的副作用就会让函数行为变得不可预测。
而函数没有副作用的话,函数行为就自然而然的可预测了。
这么想一想,要解决状态变动的可预测性问题,要么始终放弃扭转状态的函数的副作用可控,要么彻底去除扭转状态的函数的副作用。
应用上的总体开发老本与偏好
除了可预测性问题对应用上的总体开发老本的影响,状态治理库本身的偏好也会较大水平地影响应用上的总体开发老本。像 Redux 中创立一个新的 store、像 Recoil 中自定义 hook 拜访状态、像 MobX 中得当应用订阅机制,都受到库本身的偏好影响变得有些费时费力。
当因为状态治理库本身的偏好减轻了最根本的状态治理性能上的应用老本时,就会对这个状态治理库方方面面的应用产生负面影响,这是应该防止的。
状态治理的新做法
剖析好了问题,接下来就能够想一下状态治理的新做法了,也就是,如何设计一个可能解决以上两个问题的新状态治理库。对于解决状态变动的可预测性问题,下面提到的两种做法只管都可行,但出于对简洁性的谋求,先尝试一下 “彻底去除扭转状态的函数的副作用” 的做法。
对于单个状态的变动,能够援用以单个状态为入参、以新的单个状态为返回值的纯函数来实现:
function check(checkboxState: CheckboxState): CheckboxState { return { /* ... */ };}
对于多个状态的变动,能够援用以多个状态为入参、以新的多个状态为返回值的纯函数来实现:
function uncheckWithTextCleaned([checkboxState, textareaState]: [ CheckboxState, TextareaState]): [CheckboxState, TextareaState] { return [ /* ... */ ];}
同时这些函数还应该可能承受除了状态之外的更多参数:
function setText(textarea: TextareaState, text: string): TextareaState { return { /* ... */ };}
因为纯函数没有副作用,援用这些函数扭转不管单个还是多个状态都只会扭转函数申明中的状态,也就是,能够在不追踪函数体的状况下晓得会扭转什么状态。
而后,援用这些函数扭转状态的过程大抵是这样的,(1) 读取状态、(2) 将状态传入函数计算新的状态 和 (3) 写入新的状态:
const oldCheckboxState = getState(keyOfCheckboxState);const newCheckboxState = check(oldCheckboxState);setState(keyOfCheckboxState, newCheckboxState);
这个过程能够进一步封装成一个通用的函数 operate
:
operate(keyOfCheckboxState, check);operate(keyOfTextareaState, setText, '');operate([keyOfCheckboxState, keyOfTextareaState], uncheckWithTextCleaned);
这样,新状态治理库的雏形就有了。
接下来,再从加重应用老本的角度试着做一做优化。
略微看一下 operate
的第一个参数 keyOf...
,作用是 (1) 惟一标识状态。然而独自为了惟一标识状态,就申明一系列惟一的字符串常量,老本是比拟高的。而残缺定义状态,还须要 (2) 状态的默认值 和 (3) 状态的类型。如果把这三点关联在一起的话,就会发现对应到了 JS 中的一个罕用概念,Plain Old JavaScript Object(POJO)。那么,通过 POJO 来定义状态的话,应用老本就进一步加重了:
interface CheckboxState { checked: boolean;}const defOfCheckboxState: CheckboxState = { checked: false,};interface TextareaState { text: string;}const defOfTextareaState: TextareaState = { text: '',};// ...operate(defOfCheckboxState, check);operate(defOfTextareaState, setText, '');operate([defOfCheckboxState, defOfTextareaState], uncheckWithTextCleaned);
而后,再以无偏好地形式加上其余最根本的状态治理性能,(1) 获取状态、(2) 订阅状态变动 和 (3) 勾销订阅:
const checkboxState1 = snapshot(defOfCheckboxState);const textareaState1 = snapshot(defOfTextareaState);const [checkboxState2, textareaState2] = snapshot([ defOfCheckboxState, defOfTextareaState,]);const unsubscribeCheckboxStateChanges = subscribe( defOfCheckboxState, onCheckboxStateChange);const unsubscribeTextareaStateChanges = subscribe( defOfTextareaState, onTextareaStateChange);const unsubscribeCheckboxTextareaStatesChanges = subscribe( [defOfCheckboxState, defOfTextareaState], onCheckboxTextareaStatesChange);
这样,可能解决 (1) 状态变动的可预测性 和 (2) 应用上的总体开发老本 上问题的新状态治理库就大抵做好了。
瞻望
在前端开发中状态治理是十分根底但极其重要的局部,而明天恰好短少了一个状态变动可预测、应用总体成本低的状态治理库,这给前端开发带来了许多挑战。
好的前端利用须要好的状态治理,作为前端开发者的咱们兴许都能够想一想怎么做状态治理才是好的。
此外,我也试着依照下面的思路写了一个状态治理库 https://github.com/statofu/statofu ,不便一起进一步尝试。
欢送大家留言进一步探讨。