V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
fantasticsoul
V2EX  ›  问与答

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

  •  
  •   fantasticsoul · 2020-04-10 19:15:28 +08:00 · 1432 次点击
    这是一个创建于 1449 天前的主题,其中的信息可能已经有所发展或是发生改变。

    ❤ 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

    • slogan
      JavaScript 状态容器,提供可预测化的状态管理

    • 设计理念
      单一数据源,使用纯函数修改状态

    mobx & mobx-react

    • slogan:
      简单、可扩展的状态管理

    • 设计理念
      任何可以从应用程序状态派生的内容都应该派生

    concent

    • 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

    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

    • 配置在 run 函数里
    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包裹根组件,同时还收手写mapStateToPropsmapActionToProps来辅助组件获取 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

    第 1 条附言  ·  2020-04-12 12:07:25 +08:00
    v2EX 文章内容大小有限制,更多比较细节可查看
    https://juejin.im/post/5e7c18d9e51d455c2343c7c4
    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2844 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 13:43 · PVG 21:43 · LAX 06:43 · JFK 09:43
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.