❤ star me if you like concent ^_^
redux
、mobx
本身是一个独立的状态管理框架,各自有自己的抽象 api,以其他 UI 框架无关( react, vue...),本文主要说的和react
搭配使用的对比效果,所以下文里提到的redux
、mobx
暗含了react-redux
、mobx-react
这些让它们能够在react
中发挥功能的绑定库,而concent
本身是为了react
贴身打造的开发框架,数据流管理只是作为其中一项功能,附带的其他增强 react 开发体验的特性可以按需使用,后期会刨去concent
里所有与react
相关联的部分发布concent-core
,它的定位才是与redux
、mobx
相似的。
所以其实将在本文里登场的选手分别是
slogan
JavaScript 状态容器,提供可预测化的状态管理
设计理念
单一数据源,使用纯函数修改状态
slogan:
简单、可扩展的状态管理
设计理念
任何可以从应用程序状态派生的内容都应该派生
slogan:
可预测、0 入侵、渐进式、高性能的 react 开发方案
设计理念
相信融合不可变+依赖收集的开发方式是 react 的未来,增强 react 组件特性,写得更少,做得更多。
介绍完三者的背景,我们的舞台正式交给它们,开始一轮轮角逐,看谁到最后会是你最中意的范儿?
以下 5 个较量回合实战演示代码较多,此处将对比结果提前告知,方便粗读看客可以快速了解。
store 配置 | concent | mbox | redux
-|-|-|-
支持分离 | Yes | Yes | No
无根 Provider & 使用处无需显式导入 | Yes | No | No
reducer 无this
| Yes | No | Yes
store 数据或方法无需人工映射到组件 | Yes | Yes | No
redux counter 示例
mobx counter 示例
concent counter 示例
状态修改 | concent | mbox | redux -|-|-|- 基于不可变原则 | Yes | No | Yes 最短链路 | Yes | Yes | No ui 源头可追踪 | Yes | No | No 无 this | Yes | No | Yes 原子拆分&合并提交 | Yes(基于 lazy) | Yes(基于 transaction) | No
依赖收集 | concent | mbox | redux -|-|-|- 支持运行时收集依赖 | Yes | Yes | No 精准渲染 | Yes | Yes | No 无 this | Yes | No | No 只需一个 api 介入 | Yes | No | No
衍生数据 | concent | mbox | redux(reselect) -|-|-|- 自动维护计算结果之间的依赖 | Yes | Yes | No 触发读取计算结果时收集依赖 | Yes | Yes | No 计算函数无 this | Yes | No | Yes
redux computed 示例
mobx computed 示例
concent computed 示例
todo-mvc 实战
redux todo-mvc
mobx todo-mvc
concent todo-mvc
counter 作为 demo 界的靓仔被无数次推上舞台,这一次我们依然不例外,来个 counter 体验 3 个框架的开发套路是怎样的,以下 3 个版本都使用create-react-app
创建,并以多模块的方式来组织代码,力求接近真实环境的代码场景。
通过models
把按模块把功能拆到不同的 reducer 里,目录结构如下
|____models # business models
| |____index.js # 暴露 store
| |____counter # counter 模块相关的 action 、reducer
| | |____action.js
| | |____reducer.js
| |____ ... # 其他模块
|____CounterCls # 类组件
|____CounterFn # 函数组件
|____index.js # 应用入口文件
此处仅与 redux 的原始模板组织代码,实际情况可能不少开发者选择了
rematch
,dva
等基于 redux 做二次封装并改进写法的框架,但是并不妨碍我们理解 counter 实例。
构造 counter 的action
// code in models/counter/action
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
export const increase = number => {
return { type: INCREMENT, payload: number };
};
export const decrease = number => {
return { type: DECREMENT, payload: number };
};
构造 counter 的reducer
// code in models/counter/reducer
import { INCREMENT, DECREMENT } from "./action";
export default (state = { count: 0 }, action) => {
const { type, payload } = action;
switch (type) {
case INCREMENT:
return { ...state, count: state.count + payload };
case DECREMENT:
return { ...state, count: state.count - payload };
default:
return state;
}
};
合并reducer
构造store
,并注入到根组件
mport { createStore, combineReducers } from "redux";
import countReducer from "./models/counter/reducer";
const store = createStore(combineReducers({counter:countReducer}));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
使用 connect 连接 ui 与store
import React from "react";
import { connect } from "react-redux";
import { increase, decrease } from "./redux/action";
@connect(
state => ({ count: state.counter.count }),// mapStateToProps
dispatch => ({// mapDispatchToProps
increase: () => dispatch(increase(1)),
decrease: () => dispatch(decrease(1))
}),
)
class Counter extends React.Component {
render() {
const { count, increase, decrease } = this.props;
return (
<div>
<h1>Count : {count}</h1>
<button onClick={increase}>Increase</button>
<button onClick={decrease}>decrease</button>
</div>
);
}
}
export default Counter;
上面的示例书写了一个类组件,而针对现在火热的hook
,redux v7
也发布了相应的 apiuseSelector
、useDispatch
import * as React from "react";
import { useSelector, useDispatch } from "react-redux";
import * as counterAction from "models/counter/action";
const Counter = () => {
const count = useSelector(state => state.counter.count);
const dispatch = useDispatch();
const increase = () => dispatch(counterAction.increase(1));
const decrease = () => dispatch(counterAction.decrease(1));
return (
<>
<h1>Fn Count : {count}</h1>
<button onClick={increase}>Increase</button>
<button onClick={decrease}>decrease</button>
</>
);
};
export default Counter;
渲染这两个 counter,查看 redux 示例
function App() {
return (
<div className="App">
<CounterCls/>
<CounterFn/>
</div>
);
}
当应用存在多个 store 时(这里我们可以把一个 store 理解成 redux 里的一个 reducer 块,聚合了数据、衍生数据、修改行为),mobx 的 store 获取方式有多种,例如在需要用的地方直接引入放到成员变量上
import someStore from 'models/foo';// 是一个已经实例化的 store 实例
@observer
class Comp extends React.Component{
foo = someStore;
render(){
this.foo.callFn();//调方法
const text = this.foo.text;//取数据
}
}
我们此处则按照公认的最佳实践来做,即把所有 store 合成一个根 store 挂到 Provider 上,并将 Provider 包裹整个应用根组件,在使用的地方标记inject
装饰器即可,我们的目录结构最终如下,和redux
版本并无区别
|____models # business models
| |____index.js # 暴露 store
| |____counter # counter 模块相关的 store
| | |____store.js
| |____ ... # 其他模块
|____CounterCls # 类组件
|____CounterFn # 函数组件
|____index.js # 应用入口文件
构造 counter 的store
import { observable, action, computed } from "mobx";
class CounterStore {
@observable
count = 0;
@action.bound
increment() {
this.count++;
}
@action.bound
decrement() {
this.count--;
}
}
export default new CounterStore();
合并所有store
为根 store
,并注入到根组件
// code in models/index.js
import counter from './counter';
import login from './login';
export default {
counter,
login,
}
// code in index.js
import React, { Component } from "react";
import { render } from "react-dom";
import { Provider } from "mobx-react";
import store from "./models";
import CounterCls from "./CounterCls";
import CounterFn from "./CounterFn";
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
创建一个类组件
import React, { Component } from "react";
import { observer, inject } from "mobx-react";
@inject("store")
@observer
class CounterCls extends Component {
render() {
const counter = this.props.store.counter;
return (
<div>
<div> class Counter {counter.count}</div>
<button onClick={counter.increment}>+</button>
<button onClick={counter.decrement}>-</button>
</div>
);
}
}
export default CounterCls;
创建一个函数组件
import React from "react";
import { useObserver, observer } from "mobx-react";
import store from "./models";
const CounterFn = () => {
const { counter } = store;
return useObserver(() => (
<div>
<div> class Counter {counter.count}</div>
<button onClick={counter.increment}>++</button>
<button onClick={counter.decrement}>--</button>
</div>
));
};
export default CounterFn;
渲染这两个 counter,查看 mobx 示例
function App() {
return (
<div className="App">
<CounterCls/>
<CounterFn/>
</div>
);
}
concent 和 redux 一样,存在一个全局单一的根状态RootStore
,该根状态下第一层 key 用来当做模块命名空间,concent 的一个模块必需配置state
,剩下的reducer
、computed
、watch
、init
是可选项,可以按需配置,如果把 store 所有模块写到一处,最简版本的concent
示例如下
import { run, setState, getState, dispatch } from 'concent';
run({
counter:{// 配置 counter 模块
state: { count: 0 }, // [必需] 定义初始状态, 也可写为函数 ()=>({count:0})
// reducer: { ...}, // [可选] 修改状态的方法
// computed: { ...}, // [可选] 计算函数
// watch: { ...}, // [可选] 观察函数
// init: { ...}, // [可选] 异步初始化状态函数
}
});
const count = getState('counter').count;// count is: 0
// count is: 1,如果有组件属于该模块则会被触发重渲染
setState('counter', {count:count + 1});
// 如果定义了 counter.reducer 下定义了 changeCount 方法
// dispatch('counter/changeCount')
启动concent
载入 store 后,可在其它任意类组件或函数组件里注册其属于于某个指定模块或者连接多个模块
import { useConcent, register } from 'concent';
function FnComp(){
const { state, setState, dispatch } = useConcent('counter');
// return ui ...
}
@register('counter')
class ClassComp extends React.Component(){
render(){
const { state, setState, dispatch } = this.ctx;
// return ui ...
}
}
但是推荐将模块定义选项放置到各个文件中,以达到职责分明、关注点分离的效果,所以针对 counter,目录结构如下
|____models # business models
| |____index.js # 配置 store 各个模块
| |____counter # counter 模块相关
| | |____state.js # 状态
| | |____reducer.js # 修改状态的函数
| | |____index.js # 暴露 counter 模块
| |____ ... # 其他模块
|____CounterCls # 类组件
|____CounterFn # 函数组件
|____index.js # 应用入口文件
|____runConcent.js # 启动 concent
构造 counter 的state
和reducer
// code in models/counter/state.js
export default {
count: 0,
}
// code in models/counter/reducer.js
export function increase(count, moduleState) {
return { count: moduleState.count + count };
}
export function decrease(count, moduleState) {
return { count: moduleState.count - count };
}
两种方式配置 store
import counter from 'models/counter';
run({counter});
configure
接口配置, run
接口只负责启动 concent// code in runConcent.js
import { run } from 'concent';
run();
// code in models/counter/index.js
import state from './state';
import * as reducer from './reducer';
import { configure } from 'concent';
configure('counter', {state, reducer});// 配置 counter 模块
创建一个函数组件
import * as React from "react";
import { useConcent } from "concent";
const Counter = () => {
const { state, dispatch } = useConcent("counter");
const increase = () => dispatch("increase", 1);
const decrease = () => dispatch("decrease", 1);
return (
<>
<h1>Fn Count : {state.count}</h1>
<button onClick={increase}>Increase</button>
<button onClick={decrease}>decrease</button>
</>
);
};
export default Counter;
该函数组件我们是按照传统的hook
风格来写,即每次渲染执行hook
函数,利用hook
函数返回的基础接口再次定义符合当前业务需求的动作函数。
但是由于 concent 提供setup
接口,我们可以利用它只会在初始渲染前执行一次的能力,将这些动作函数放置到setup
内部定义为静态函数,避免重复定义,所以一个更好的函数组件应为
import * as React from "react";
import { useConcent } from "concent";
export const setup = ctx => {
return {
// better than ctx.dispatch('increase', 1);
increase: () => ctx.moduleReducer.increase(1),
decrease: () => ctx.moduleReducer.decrease(1)
};
};
const CounterBetter = () => {
const { state, settings } = useConcent({ module: "counter", setup });
const { increase, decrease } = settings;
// return ui...
};
export default CounterBetter;
创建一个类组件,复用setup
里的逻辑
import React from "react";
import { register } from "concent";
import { setup } from './CounterFn';
@register({module:'counter', setup})
class Counter extends React.Component {
render() {
// this.state 和 this.ctx.state 取值效果是一样的
const { state, settings } = this.ctx;
// return ui...
}
}
export default Counter;
渲染这两个 counter,查看 concent 示例
function App() {
return (
<div className="App">
<CounterCls />
<CounterFn />
</div>
);
}
此回合里展示了 3 个框架对定义多模块状态时,不同的代码组织与结构
redux
通过combineReducers
配合Provider
包裹根组件,同时还收手写mapStateToProps
和mapActionToProps
来辅助组件获取 store 的数据和方法mobx
通过合并多个subStore
到一个store
对象并配合Provider
包裹根组件,store 的数据和方法可直接获取concent
通过run
接口集中配置或者configure
接口分离式的配置,store 的数据和方法可直接获取store 配置 | concent | mbox | redux
-|-|-|-
支持分离 | Yes | Yes | No
无根 Provider & 使用处无需显式导入 | Yes | No | No
reducer 无this
| Yes | No | Yes
store 数据或方法无需人工映射到组件 | Yes | Yes | No