redux、mobx、concent 特性大比拼, 看后生如何对局前辈(1)

2020-04-10 19:15:28 +08:00
 fantasticsoul

❤ star me if you like concent ^_^

序言

reduxmobx本身是一个独立的状态管理框架,各自有自己的抽象 api,以其他 UI 框架无关( react, vue...),本文主要说的和react搭配使用的对比效果,所以下文里提到的reduxmobx暗含了react-reduxmobx-react这些让它们能够在react中发挥功能的绑定库,而concent本身是为了react贴身打造的开发框架,数据流管理只是作为其中一项功能,附带的其他增强 react 开发体验的特性可以按需使用,后期会刨去concent里所有与react相关联的部分发布concent-core,它的定位才是与reduxmobx 相似的。

所以其实将在本文里登场的选手分别是

redux & react-redux

mobx & mobx-react

concent

介绍完三者的背景,我们的舞台正式交给它们,开始一轮轮角逐,看谁到最后会是你最中意的范儿?

结果预览

以下 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

mobx 示例
concent 示例


衍生数据 | 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

round 1 - 代码风格初体验

counter 作为 demo 界的靓仔被无数次推上舞台,这一次我们依然不例外,来个 counter 体验 3 个框架的开发套路是怎样的,以下 3 个版本都使用create-react-app创建,并以多模块的方式来组织代码,力求接近真实环境的代码场景。

redux(action 、reducer)

通过models把按模块把功能拆到不同的 reducer 里,目录结构如下

|____models             # business models
| |____index.js         # 暴露 store
| |____counter          # counter 模块相关的 action 、reducer
| | |____action.js     
| | |____reducer.js     
| |____ ...             # 其他模块
|____CounterCls         # 类组件
|____CounterFn          # 函数组件
|____index.js           # 应用入口文件

此处仅与 redux 的原始模板组织代码,实际情况可能不少开发者选择了rematchdva等基于 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;

上面的示例书写了一个类组件,而针对现在火热的hookredux v7也发布了相应的 apiuseSelectoruseDispatch

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

mobx(store, inject)

当应用存在多个 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(reducer, register)

concent 和 redux 一样,存在一个全局单一的根状态RootStore,该根状态下第一层 key 用来当做模块命名空间,concent 的一个模块必需配置state,剩下的reducercomputedwatchinit是可选项,可以按需配置,如果把 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 的statereducer

// 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});
// 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 个框架对定义多模块状态时,不同的代码组织与结构

store 配置 | concent | mbox | redux -|-|-|- 支持分离 | Yes | Yes | No 无根 Provider & 使用处无需显式导入 | Yes | No | No reducer 无this | Yes | No | Yes store 数据或方法无需人工映射到组件 | Yes | Yes | No

1469 次点击
所在节点    问与答
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/661278

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX