caorich

4 个月前
  • 1628

    浏览
  • 0

    评论
  • 0

    收藏

觉得mobx不错,但又放不下redux?

本文作者:ivweb caorich 原文出处:IVWEB社区 未经同意,禁止转载     IVWEB公众群2

react的状态管理

说到react的状态管理工具,大家都会想到redux或者mobx。

redux || mobx     // => true

redux

redux出现较早,包括我们项目组在内,redux几乎已经成了react工程的标配。

redux带来的事件分发机制,将复杂的操作分发到各个reducer,有一种大事化小的睿智,确实将复杂的数据更改逻辑解耦得足够简单。包括我leader在内的很多同学都觉得redux的事件分发机制对于现代前端工程是再适合不过的了。

重绘

但redux的缺点也是足够明显的。每一次dispatch事件之后都会导致整个虚拟dom至顶向下的重绘。重绘剪枝需要在shouldComponentUpdate中完成,如果事件足够复杂, store足够大,shouldComponentUpdate方法的剪枝粒度就不那么容易控制了(实际情况下,shouldComponentUpdate基本和TODO一样不可保证)。

reducer

redux的另一个缺点是:reducer要求每次返回一个新的对象引用。当需要修改的数据层级较深,reducer写起来很难保证优雅。例如有如下store结构:

var state = {
    list: [{
        baseInfo: {
            anchorUid: 12312321,
            anchorLogo: '…'
        },
        roomInfo: {
            rateList: [{
                id: 324234,
                score: 100
            }]
        }
    }]
}

如果需要更改state.list[0].roomInfo.rateList[0].score = 90。这个reducer应该怎么写呢?如果用原生的js应该是这样:

let newRate = Object.assign({}, state.list[0].roomInfo.rateList[0])
newRate.score = 90;
let newRateList = state.list[0].roomInfo.rateList.slice();
newRateList[0] = newRate;
let newRoomInfo = Object.assign({}, state.list[0].roomInfo)
newRoomInfo.rateList = newRateList;
//...

这样的代码看起来像是吃坏了肚子一般。

所以一般redux项目都会刻意的保持store的平坦化,没有深层级的数据,用Object.assign几步搞定。

如果store不可避免的太大了,怎么办呢?很多工程开始使用Immutable.js,以上的代码可以改写为:

let newState = state.updateIn(['list',0,'roomInfo','rateList',0, 'score'], 90);

store 大了,你不用immutable还能怎么办呢?

瞬间感觉高大上!于是我经不住诱惑也npm i immutable -S了。结果被它api恶心到了,最后卸载决定还是用Object.assign

Mobx

总结一下,上一节列出的redux的两个缺点:

  • 每次dispatch触发至顶向下的重绘
  • 新的state对象引用难于构造

新出现的mobx带来激动人心的特性,刚好解决这两个问题。请看下面的例子:

import {Provider} from 'mobx-react'

let appInfo = mobx.observable(state)  // 这个state就是上一节例子中提到的state

ReactDom.render(
      <Provider appInfo={appInfo}>
        <App />
      </Provider>,
  document.getElementbyId('container')
);

P. S. 这里隐藏了<App />的实现细节。

第一点,mobx中数据的每一次更新,都会定点的重绘特定组件,整个过程不需要shouldComponentUpdate的参与。<App />中的所有组件都不在需要再管理重绘剪枝。

第二点,如果需要更新内层数据,只需像下方的代码一样,直接赋值。重绘操作会自动进行:

appInfo.list[0].roomInfo.rateList[0].score = 90;

这样的开发体验简直跟做梦一样。

P.S. 更加详细的例子可以去mobx的官网上下载,这篇文章的重点并不是介绍mobx的使用方法。

问题来了

既然mobx这么方便和magic。它又有什么缺点呢?

在实践中,一个问题一直困扰着我:

mobx并没有提供一套数据层的更新模型,可以在用户事件句柄中直接更改数据,也可以代理给其他方法。那怎样做才是最佳实践?怎样才能更好的解耦?

是不是应该创建一个controller,用controller统一处理用户事件、统一管理应用状态。回到我们在MVC架构的时代?于是我默默动手写了下面的代码:

class Controller{
  constructor(store){
       this.store = store 
  } 
  @action.bound
  setRateScore(index, val){
    this.store.appInfo.rateList[index].score = val;
  }
}

这样可以将数据层与业务逻辑解耦,不需要将繁重的业务逻辑交给mobx来完成。

然而,我的leader拒绝了这种想法。原因是,controller是强逻辑的,也就是说,所有用户事件和数据管理都交给了controller,造成了controller臃肿,同时controller和mobx强耦合,mobx数据层对象变更了,controller就会报错。

反观redux中的事件管理机制,所有事件都被分发到细粒度的reducer上,至于这个reducer怎么处理,事件发送者并不清楚。这一点在大型工程中十分重要。

mobx适合小工程,大工程还是得上redux

难怪网上很多相关的论调,觉得mobx不适合大型工程,多数同学仍然持有redux不放。这种见解过于片面,不过也暴露了mobx在使用上鸡肋的地方。

那么,对于已经用惯了redux的前端猿们,我们是否可以即使用mobx,又同时保持redux的事件分发机制不变呢?

解法1:同时使用redux和mobx

mobx的开发者也开始注意到,mobx主要是作为一个响应式的数据结构而存在,虽然它总是和redux相提并论,其实两者并不冲突,mobx实质上并没有抢redux的生意!

怎么理解呢?回到传统的MVC上来看,redux的工作类似于一个controller,而mobx的工作类似于model。redux负责分发事件,reducer并没有限定store对象就是一个简单的js对象,可以用immutable,那也肯定可以用mobx。

mobx官方的MST已经提供了这样的支持,官方的opinion 以及 demo。我们可以将store替换成一个MST对象,MST对象本质上是immutable的数据类型,这样在reducer中可以避免繁琐的Object.assign代码,这个用法与你使用Immutable.js别无二致。在redux中引入MST很简单,几乎无痛。简要用法如下:

import { asReduxStore } from "mst-middlewares"
import { Provider } from "react-redux"  //使用redux的privider

const todos = todosFactory.create({})
const store = asReduxStore(todos)  // 但使用mobx的store

render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById("root")
)

action维持不变,reducer被改写为更加方便的形式:

// reducer的写法
todo.actions(self=>({
    [DELETE_TODO]({ id }) {
        const todo = self.findTodoById(id)
        self.todos.remove(todo)
    },
    [EDIT_TODO]({ id, text }) {
        self.findTodoById(id).text = text
    },
    [COMPLETE_TODO]({ id }) {
        const todo = self.findTodoById(id)
        todo.completed = !todo.completed
    }
}))

这个解法,相当于mobx抢了Immutable.js的生意,如果开发者想继续用redux,但是(和我一样)对Immutable.js的api深恶痛绝的话,不妨试试这种方法,开发体验顺滑了不少, ೭(˵¯̴͒ꇴ¯̴͒˵)౨”。

缺点是:数据更新仍然由redux控制,自顶向下的重绘开销不小,剪枝操作复杂而没有保证。

解法2:实现数据分发层

如果完全去掉redux,改用mobx-react进行页面重绘,就可以达到精确的重绘定位。剩下的工作就是我们自己实现一套redux的数据分发逻辑。

这里提供一个简单的版本供参考:

先定义一个Dispatcher类,对外暴露dispatch方法和setMiddleware方法。

class Dispatcher{
    constructor(store){
        this._store = store;
        this.dispatch = this.dispatch.bind(this);
    }
    init(store){
        this._store = store;
    }
    setMiddleware(...args){
        if(!args.length) return;
        let _args = args.reverse();
        let dispatch = this.dispatch;
        _args.forEach(mid=>{
            dispatch = mid(this)(dispatch);
        })
        this.dispatch = dispatch;
    }
    dispatch(opts){
        let keys = Object.keys(this._store)
        keys.forEach(key=>{
            let item = this._store[key];
            if(item.reducer){
                item.reducer(opts);
            }
        });
    }
}

然后在mobx的数据模型中定义一个reducer方法,将原有的reducer逻辑照搬过来,例如:

let Count = types.model({
    count: types.number
}).actions(self=>({
  reducer(action){
    switch(action.type){
      case 'ADD_COUNT':
        self.count += 1;
        break;
      case 'DE_COUNT':
        self.count -= 1;
        break;
    }
  }
}))

大功告成。可以继续沿用redux中的action和middleware代码,照搬无误,例如:

let dispatcher = new Dispatcher(store);
dispatcher.setMiddleware(cgiFetch, login, report);

// index.jsx
addClick = ()=>{
  dispatcher.dispatch({type: 'ADD_COUNT'})
}
deClick = ()=>{
  dispatcher.dispatch({type: 'DE_COUNT'})
}
render(){
  return <div>
    <div>{this.props.Count.count}</div>
    <span onClick={this.addClick}>+</span>
    <span onClick={this.deClick}>-</span>
  </div>
}

P.S. 以上代码中的dispatcher的实现,中间件部分逻辑没有封装getStore方法,实际情况需要自己加上。

最后。本文提到只是自己在工程实践中得出的一些总结,绝非唯一的架构方法。欢迎找我谈论,欢迎大大们评论指导。

0 条评论