react的状态管理
说到react的状态管理工具,大家都会想到redux或者mobx。
代码语言:javascript复制redux || mobx // => true
redux
redux出现较早,包括我们项目组在内,redux几乎已经成了react工程的标配。
redux带来的事件分发机制,将复杂的操作分发到各个reducer,有一种大事化小的睿智,确实将复杂的数据更改逻辑解耦得足够简单。包括我leader在内的很多同学都觉得redux的事件分发机制对于现代前端工程是再适合不过的了。
重绘
但redux的缺点也是足够明显的。每一次dispatch事件之后都会导致整个虚拟dom至顶向下的重绘。重绘剪枝需要在shouldComponentUpdate
中完成,如果事件足够复杂, store足够大,shouldComponentUpdate
方法的剪枝粒度就不那么容易控制了(实际情况下,shouldComponentUpdate
基本和TODO
一样不可保证)。
reducer
redux的另一个缺点是:reducer要求每次返回一个新的对象引用。当需要修改的数据层级较深,reducer写起来很难保证优雅。例如有如下store结构:
代码语言:javascript复制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带来激动人心的特性,刚好解决这两个问题。请看下面的例子:
代码语言:javascript复制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 />
中的所有组件都不在需要再管理重绘剪枝。
第二点,如果需要更新内层数据,只需像下方的代码一样,直接赋值。重绘操作会自动进行:
代码语言:javascript复制appInfo.list[0].roomInfo.rateList[0].score = 90;
这样的开发体验简直跟做梦一样。
P.S. 更加详细的例子可以去mobx的官网上下载,这篇文章的重点并不是介绍mobx的使用方法。
问题来了
既然mobx这么方便和magic。它又有什么缺点呢?
在实践中,一个问题一直困扰着我:
mobx并没有提供一套数据层的更新模型,可以在用户事件句柄中直接更改数据,也可以代理给其他方法。那怎样做才是最佳实践?怎样才能更好的解耦?
是不是应该创建一个controller,用controller统一处理用户事件、统一管理应用状态。回到我们在MVC架构的时代?于是我默默动手写了下面的代码:
代码语言:javascript复制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被改写为更加方便的形式:
代码语言:javascript复制// 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逻辑照搬过来,例如:
代码语言:javascript复制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代码,照搬无误,例如:
代码语言:javascript复制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方法,实际情况需要自己加上。
最后。本文提到只是自己在工程实践中得出的一些总结,绝非唯一的架构方法。欢迎找我谈论,欢迎大大们评论指导。