Redux和Mobx:AK47和匕首
此前一直未从0开始写过Redux的工程,近日想简单对比下Redux和Mobx的各自特点,于是动手撸了TodoList,感受了它们的不同。对比上看
- Redux可靠规整,有一整套最佳实践,写大型应用时能避免很多坑
- MobX轻便锋利,概念不多上手容易,在中小型应用中开发效率更高
Redux
Redux吸收了Flux和Elm的设计特点,正如它在Three Principles中写到的那样,唯一可信数据源,状态数据只读,状态改变为纯函数,这三大特点最大可能提升了可预测性,减少了调试的难度。同时,在概念上也易于理解。不过,复杂的设定和较多的代码入侵使得个人项目使用时稍显笨重,团队项目使用在改动已有代码时又会有牵一发动全身的感觉。
在数据流上,Redux规定action描述state的改变情况,reducer根据action
定义state如何更新。
- action,由type和payload部分组成,描述发生了什么变化,如
{ type: 'ADD_TODO', text: 'Eat pie.' }
- reducer,接受state和action作为入参,返回一个全新的state,正如文档里所说Redux assumes you never mutate your data
在这种设计理念下,我们借助redux创建state,之后所有的状态更新,都通过state.dispatch提交action完成,再借由connect等工具同步更新组件的props实现数据绑定的效果。除了设计理念外,Redux一些工程实践上的设计也值得一提
和React的互动
Redux并不是和React绑定了,但确实经常和React同时出现。react-redux是用来和react绑定的库。在结合了Redux后,React会有一些最佳工程实践
- 区分开绘制组件和容器组件,前者只负责将数据转化为标签,后者负责接入数据,完成逻辑,组织绘制组件,并向其传递用户交互触发dispatch的函数。这样的层级设计将会增加解耦,减少后期修改时的工作量
- 在最外侧调用
createStore
生成store并传入<Provider>
组件,内部的容器组件,通过connect
,mapStateToProps
,mapDispatchToProps
就能便利地和store进行沟通,就像下面这样
1 | const mapStateToProps = (state, ownProps) => { |
Action Creator
使用函数创建标准化的action,而不要把action直接写在dispatch内。就像下面这样
1 | function addTodo(text) { |
这么做在action发生更改时,只需要修改定义函数的位置即可,简单方便。可以发现上面的action creator中,完成的工作只是简单的组装type和函数的入参在同一个action object里。当这样的函数很多时,还可以用action creator creator来帮我们一行生成这些类似的action creator。
1 | const createActionCreator = (type, ...argNames) => { |
Split Reducer
除了上面讲到的Action Creator外,split reducer页很常见,它的应用场景出现在当state比较复杂时,可以针对state的每个field单独写reducer,然后通过Redux的combineReducers
组合起来。
就像TodoList中,筛选条件filter
和待办事项todos
可以拆出两个reducer去更改。
1 | const todos = (state = [], action) => { |
Middleware和Async Action
首先,我们要明确一点,Redux中只有dispatch能改变store,然而默认情况下,dispatch只接受action。因此,当我们想异步修改store时,异步的逻辑只能写在组件里(Vuex里则可以通过action异步提交commit)。设想一下,假如一个fetch
API的逻辑在多处都用到时,只能在这些地方重复书写这些代码。好在,Redux提供了middleware的概念,和Express中的中间件类似,不同的是Redux中中间件处理的是用户提交的dispatch请求而已。
文档里对中间件的介绍非常到位,鉴于我们对express以及co中类似概念的了解,我们直接从第4步看起:
1 | function logger(store) { |
可以看到,原理其实是类似的,关键点在于使用store的dispatch
方法依次暂存上一个节点,这么做的好处是保证最终能抛出最后真正的dispatch,且能实现链式的效果。然而,使用dispatch显得还不自然,于是就有了下面的版本,每次将next主动传入,当让applyMiddleware里需要额外传入next。
1 | const logger = store => next => action => { |
借助中间件的帮助,我们可以在dispatch前完成我们想要的操作:打log,catch错误,甚至提前终止流程。而异步dispatch正是借助了thunk中间件的帮助。它的实现很简单。
1 | const thunk = store => next => action => |
在我们通过applyMiddleware
注入到store中后,就可以在dispatch
中写入函数了!就像下面这样
1 | function fetchPosts(subreddit) { |
在使用时正常dispatch即可。
1 | dispatch(fetchPostsIfNeeded(selectedSubreddit)) |
其他常用的store API
getState()
获取当前的state状态subscribe(listener)
在每次更改state时触发createStore()
根据输入的reducer,initState,middleware等生成store
脚手架
官网在介绍时列举了许多依赖库,却并没有给出示例的脚手架。没有舒服的脚手架,效率和工作热情都会受影响。这里介绍一种使用create-react-app快速搭建Redux脚手架的过程,下面的Mobx类似。
项目依赖大概有下面这些
- react
- redux
- react-redux
- react-router/history
- webpack 打包工具
- babel polyfill方案
- eslint JS代码风格标准化
- stylelint CSS代码风格标准化
- postcss
但其实create-react-app可以帮你搭好其中包括react、react-dom、eslint、babel、webpack、postcss等绝大多数依赖环境,且完成配置。剩下的react-redux和react-router手动安装即可。
create-react-app类似于vue-cli,创建的默认配置不满意时,还可以npm run eject
将默认配置撤销成用户配置,交给用户自己配置。
MobX
对比Redux,诞生于2015年3月的MobX在概念上吸收Vue,Knockout等MVVM框架要更多一些1,号称是TFRP(Transparent Functional Reactive Programming)。Transparent在依赖项的更新是隐式完成的,Functional在Computed中的使用,Reactive就不说了。MobX原名Mobservable,而后改名为MobX,官方并未说明如何发音,姑且读作moʊ-bex。和Redux对比来看,巧合的是MobX的设计理念也可以分为三部分:
- 将所有的状态抽出来,用observable修饰
- 描述出状态到视图的映射关系,这个过程因框架而异,但是一般React多一些,使用observer修饰
- 在要修改状态的位置使用action包裹(强烈建议你这么做)
不过还有很核心的一点,文档里并没有提到
- 把所有state修改的副作用放在
autorun/reaction/when
体内,在必要时在体内继续使用action
哇,是不是简洁了很多。你对observable对象做的所有修改(不论有没有用action包裹)都会自动反映在视图中,在项目结构上,你完全可以根据自己需要组织。不过,没有Redux里transaction的概念,MobX中对状态的修改在时间上都是不可回溯的。同时,没有中间件的概念,意味着在状态比较复杂时,可维护性就会下降。
整个MobX的关键API主要是由下面几部分组成的
- observable 创建被依赖项,在设计中即state
- computed 被依赖项的计算值,和Vue中的computed属性一致
- action 动作,用来修改state,显式的使用可以使逻辑更清楚,当然不在action里修改observable也是允许的
- observer和autorun/reaction/when,前者是derivation即根据state衍生出的结果,后者是reaction即state变化会触发的副作用(如IO等)
与React的结合
MobX和React相结合的方式就自由了很多。大体上使用components
存储组件,stores
描述状态。
在stores
描述状态时,
- 通过
@observable
描述需要响应变化的状态变量,同时尽量将所有改变状态变量的操作封装成整个class的方法(并不强制),便于管理。 - 通过
@computed
声明能够直接根据当前状态变量得到的衍生值。有意思的是,MobX只在observable变化时更新这个值,而不是在用户需要时去计算,从而节省了许多时间 - 通过
autorun()
,reaction()
,when()
声明式地定义状态改变时的side effect,它们的执行结果都会返回一个dispose函数,在它们的生命周期结束后方便显式垃圾回收。它们三个都是除了用户操作外几乎唯一能进行副作用操作的地方,其中autorun()
在定义时就会被执行一次reaction()
仅在变化时执行,且对函数内部改变状态不敏感when()
则是在变化时执行一次即失效
- 通过
@action
描述对状态做修改的行为,推荐使用,修饰在方法名前,开启strict模式后,则是强制要求使用
上面这些关键字都有修饰词(如:@action.bound
)还有对应的ES5语法。(如:@observable key = value
等同于extendObservable(this, { key: value })
)。MobX并不要求使用单一的状态树,可以用多个文字组织你的状态。其中的一个store文件可能像下面这样:
1 | class TodoList { |
components
文件夹下做的事和使用其他框架其实差别不大。区别主要在引入observer后,用@observer
装饰组件类。需要修改状态时,可以直接对props中传入的store进行
修改(不过还是建议使用store中定义好的方法修改),视图就会同步更新,副作用也会同步完成。一个使用了mobx-react的组件大概像下面这样
1 | @observer |
除了直接使用store外,mobx-react还提供了将observable对象通过<Provider>
和inject
2传入组件的方式。其他的用法可以参看文档,还有中文版翻译。
脚手架
除了Redux里面提到的,因为MobX中用到了最新的decorator特性,.babelrc
配置文件大概是下面这样
1 | { |
package.json
中需要额外引入”mobx”和”mobx-react”两个库(至少)。官方还提供了mobx-react-boilerplate,这些环境都已帮你配置好,按照README.md
操作即可。另外,官方提供的awesome list是一个非常好学习mobx的地方