背景
由于react追求通过组合不同粒度的组件来展示视图,对于功能复杂的应用来说,组件间逻辑复用和状态共享的问题油然而生,对于逻辑复用,react从render props+HOC到custom hooks,基本可以说是十分契合react基于组合的编程模型了;对于状态共享,当下最火的方案,当属基于redux和mobx的全局状态管理方案了,但在hooks出现后,基于hooks和context的方案在社区中的呼声亦不容小觑,就在这样的背景下,一个基于该方案的轻量级的react状态管理——co-store诞生了!下面就一起走进科学,看看这几种方案在服务react项目中的对比吧~
Part1:设计与接入
redux
-
设计
Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen(Redux试图通过对更新的方式和时间施加一定的限制来使状态突变可预测)围绕这个思想,redux设计了下面几个核心概念
- store:“存”,是集中保存状态的地方
- action:“改”,一个普通的js对象,是改变状态的唯一办法
- reducer:“变”一个纯函数,根据action完成改变store中对应的状态
- middleware:“强”,中间件,可以理解成对action预处理,也可以理解成增强store
-
接入
由于redux是与框架无关的全局状态管理方案,在react应用中接入必须配合react-redux
// 创建 const rootReducer = (state, action) => { switch (action.type) { case 'XXX': return newState default: return state } } const store = createStore(rootReducer); // 接入React <Provider store={store}>...</Provider>
mobx
-
设计
Reacting to state changes is always better then acting on state changes.(对状态变化做出反应总是比对状态变化采取行动更好)围绕这个思想,mobx设计了下面几个核心概念
- state: 驱动应用的数据,像是有数据的excel表格
- derivations: 任何源自状态并且不会再有任何进一步的相互作用的东西就是衍生
- computed values: 计算值,从当前可观察状态中衍生出的值, excel的公式就是计算值的衍生
- reactions:当状态改变时需要自动发生的副作用
- action: 任一一段可以改变状态的代码,像在excel单元格中输入一个新的值就可以称为action
-
接入
由于mobx是与框架无关的状态管理方案,在react应用中接入必须配合mobx-react(或者mobx-react-lite)
// 创建 const store = () => useLocalStore(() => { return { ...state, ...actions } }) // 接入React <Provider stores={stores}>...</Provider>
co-store
-
设计
co-store attempts to define store according to function to converge state as much as possible.(co-store尝试根据功能定义store来尽可能地收敛状态)co-store类比custom hooks,强调以功能为最小单元去创建store,当且仅当你的功能需要在全局或组件间共享状态时才有必要去定义store,否则尽可能地收敛状态到对应的组件内。然后通过组合组件去生成UI,通过组合hooks去定制逻辑。
围绕这个思想,co-store设计了下面几个核心概念
- state:存储store的所有状态
- action:是改变状态的唯一办法
- subscription:支持选择性订阅状态和衍生数据
-
接入
// 创建 const initialState = xxx const actionCreators = { xxx: () => xxx } const store = createScopeStore(initialState, actionCreators) // 接入React <store.Provider>...</store.Provider>
summary
| 设计与接入 | redux | mobx | co-store |
|---|---|---|---|
| 不需要配合库 | react-redux | mobx-react(mobx-react-lite) | ✅ |
| 支持拆分store | ❌ | ✅ | ✅ |
| 思想 | 使状态突变可预测 | 对状态变化做出反应 | 尽可能地收敛状态 |
Part2:状态读写
redux
通过定义mapStateToProps和mapDispatchToProps作为react-redux提供的connect方法的参数,把redux中store中的state和dispatch作为props传递到组件中。换句话说,react中的redux,通过props实现状态读写。
const mapStateToProps = state => ({...})
const mapDispatchToProps = dispatch => ({...})
const Component = (props) => (...)
export default connect(mapStateToProps, mapDispatchToProps)(Component)
mobx
通过Context把定义的stores组合起来,通过useStores(基于useContext)来获取对应的store,通过包装useObserver(mobx-react-lite)来订阅状态,通过直接调用store中属性(包括state和action)来改变状态,mobx会对变更的状态做出正确的响应。换句话说,react中的mobx,通过被观察的对象与this来实现状态读写。
const {xxxStore} = useStores()
const Component = (props) => useObserver(() => (...))
export default Component
co-store
每一个store都具备特定的功能,直接引入,通过store提供的useStore、useActions来实现状态的读写。
const [state, actions] = xxxStore.useStore()
const Component = (props) => (...)
export default Component
summary
| 状态读写 | redux | mobx | co-store |
|---|---|---|---|
| 无this | ✅ | ❌ | ✅ |
| 状态不可变 | ✅ | ❌ | ✅ |
| 不需要手动映射 | ❌ | ✅ | ✅ |
| 原生支持异步 | ❌ | ✅ | ✅ |
Part3:选择性订阅和衍生数据
redux
redux结合react-redux实现的状态管理本身并不支持选择性订阅和衍生数据,状态作为props传入组件,需要组件本身去做优化。
mobx
mobx通过对组件包装useObserver,可以实现只有被订阅的state节点变化时,才进行对应的更新,这样就实现了选择性订阅;在定义store时通过定义属性的get方法来实现衍生数据(计算属性)。
// 当xxxStore.state1的变化时,组件才会进行对应的更新
const {xxxStore} = useStores()
const Component = (props) => useObserver(() => (...xxxStore.state1...))
// 衍生数据(计算属性)computedState1,当依赖的state1变化时会进行对应的更新
const store = useLocalStore(() => {
return {
...state,
get computedState1(){
return this.state.state1...
}
...actions
}
})
co-store
与通过功能定义store的思想一致,co-store需要在创建时提前声明subscriptions,然后使用时通过store提供的useSubscribe来实现选择性订阅的状态和衍生数据
// 选择性订阅的状态和衍生数据
const subscriptions = {
// 只订阅了state1,只有state1改变了才会重渲染
state1: (state) => state.state1,
// 订阅了computedState1,当依赖的state1变化时才会重渲染
computedState1: (state) => state.state1...
}
const store = createScopeStore(initialState, actionCreators, subscriptions)
// 使用
const state1 = xxxStore.useSubscribe("state1")
const computedState1 = xxxStore.useSubscribe("computedState1")
summary
| 选择性订阅和衍生数据 | redux | mobx | co-store |
|---|---|---|---|
| 无this | ✅ | ❌ | ✅ |
| 定制更新 | ❌ | ✅ | ✅ |
| 衍生数据(计算属性) | ❌ | ✅ | ✅ |
Part4:性能对比
性能对比其实非常麻烦,因为很难找到一个标准去衡量,查找了不少资料,觉得Mobx 与 Redux 的性能对比这篇文章的对比场景还是可以借鉴的,下面就基于这个场景,直接对比一下在1000个组件同时订阅同一个store场景下mobx和co-store的耗时:
不难看出,如果针对上述的场景,mobx和co-store的表现没有太大的差别(可以假装得出co-store≈mobx > redux的结论),这里的对比其实也是针对场景,不算很严格,只是为了说明基于Context实现的store在性能上也不差,毕竟是react官方出品~
Part5:实战·Todos
redux
直接奉上官网的例子:redux-todos
mobx
codesandbox:mobx-todos
co-store
codesandbox:co-store-todos
写在最后
基于redux和mobx的全局状态管理方案,推荐把状态都放在store去管理,从某种程度来说,与React中custom hooks根据功能定制逻辑的思想似乎不太契合;现在也有不少将custom hooks封装成store的库,大概的思路就是去每个组件都有自己的状态,当一个组件状态更新时,通过context+发布订阅模式去决定其他组件是否需要更新,这样做的好处是简介明了,但是store概念不太清晰,功能复杂一点的hooks很有可能把状态和副作用以及其他逻辑冗杂在一起,另外,拆分多个hooks去组合store,在store相互依赖时需要手动维护对应Provider的顺序问题;
说了这么多不好,按照套路,这时候应该宣传一波co-store了!然而,co-store的理念是尽可能地收敛你的状态到对应的组件中,换句话说,在写store之前先确定你是否真的需要它,根据需求场景和功能去决定你是否需要状态管理,如果需要,再去寻求解决该场景的最佳方案,如果需求场景(如应用设计层)迫切地需要event sourcing这种时间回溯能力,redux是一个很不错的选择;如果你习惯了MVC那套抽离view和数据层,并且在this的海洋穿梭自如,或许基于功能的声明式的store的不是你的菜,mobx在这方面的造诣已经很高了。
而对于co-store,它的定位就是react大力推广的hooks+function component模式(其实通过封装,也可以支持class component),正如背景所说,co-store解决的不是逻辑封装的问题,而是状态共享的问题,根据功能封装你的逻辑,根据功能定义你的store,我想这种基于这种模式去组合声明式的UI,或许是react在践行代数效应里的最佳实践吧~
关于代数效应,我安利大牛的代数效应与React这篇文章。另外,如果你对co-store感兴趣,奉上源码传送门,欢迎一起探讨交流~