由于 redux 需要写很多繁琐的 action 和 reducer,大部分项目也没有复杂到需要用到 redux 的程度,导致不少人对 redux 深恶痛绝。mobx 是另一种状态管理方案,这里分享一下我最近使用 mobx 的经验。
更响应式
我最喜欢 mobx 的地方就是和 vue 一样的数据监听,底层通过 Object.defineProperty 或 Proxy 来劫持数据,对组件可以进行更细粒度的渲染。
在 react 中反而把更新组件的操作(setState)交给了使用者,由于 setState 的"异步"特性导致了没法立刻拿到更新后的 state。
computed
想像一下,在 redux 中,如果一个值A是由另外几个值B、C、D计算出来的,在 store 中该怎么实现?
如果要实现这么一个功能,最麻烦的做法是在所有B、C、D变化的地方重新计算得出A,最后存入 store。
当然我也可以在组件渲染A的地方根据B、C、D计算出A,但是这样会把逻辑和组件耦合到一起,如果我需要在其他地方用到A怎么办?
我甚至还可以在所有 connect 的地方计算A,最后传入组件。但由于 redux 监听的是整个 store 的变化,所以无法准确的监听到B、C、D变化后才重新计算A。
但是 mobx 中提供了 computed 来解决这个问题。正如 mobx 官方介绍的一样,computed 是基于现有状态或计算值衍生出的值,如下面 todoList 的例子,一旦已完成事项数量改变,那么 completedCount 会自动更新。
class TodoStore {
@observable todos = []
@computed get completedCount() {
return (this.todos.filter(todo => todo.isCompleted) || []).length
}
}
reaction
reaction 则是和 autorun 功能类似,但是 autorun 会立即执行一次,而 reaction 不会,使用 reaction 可以在监听到指定数据变化的时候执行一些操作,有利于和副作用代码解耦。
// 当todos改变的时候将其存入缓存
reaction(
() => toJS(this.todos),
(todos) => localStorage.setItem('mobx-react-todomvc-todos', JSON.stringify({ todos }))
)
依赖收集
在 mobx 中,通过 autorun 和 reaction 对依赖的数据进行了收集(可以通过 get 来收集),一旦这些数据发生了变化,就会执行接受到的函数,和发布订阅很相似。
mobx-react 中则提供了 observer 方法,用来收集组件依赖的数据,一旦这些数据变化,就会触发组件的重新渲染。
管理局部状态
在 react 中,我们更新状态需要使用 setState,但是 setState 后并不能立马拿到更新后的 state,虽然 setState 提供了一个回调函数,我们也可以用 Promise 来包一层,但终究还是个异步的方式。
在 mobx 中,我们可以直接在 react 的 class 里面用 observable 声明属性来代替 state,这样可以立马拿到更新后的值,而且 observer 会做一些优化,避免了频繁 render。
@observer
class App extends React.Component {
@observable count = 0;
constructor(props) {
super(props);
}
@action
componentDidMount() {
this.count = 1;
this.count = 2;
this.count = 3;
}
render() {
return <h1>{this.count}</h1>
}
}
拆分 store
mobx 中的 store 的创建偏向于面向对象的形式,mobx 官方给出的例子 todomvc 中的 store 更接近于 MVC 中的 Model。
但是这样也会带来一个问题,业务逻辑我们应该放到哪里?如果也放到 store 里面很容易造成不同 store 之间数据的耦合,因为业务代码必然会耦合不同的数据。
我参考了 dobjs 后,推荐将 store 拆分为 actions 和 models 两种。
actions 和 models 一起组合成了页面的总 store,models 只存放UI数据以及只涉及自身数据变化的 action 操作( 在mobx 严格模式中,修改数据一定要用 action 或 flow)。
actions store 则是负责存放一些需要使用来自不同 store 数据的 action 操作。 我个人理解,models 更像 MVC 中的 Model,action store 是 Controller,react components 则是 View,三者构成了 MVC 的结构。
- stores
- actions
- hotelListAction.js
- dataModel
- globalStatus.js
- hotelList.js
- index.js
// globalStatus
class GlobalStatus {
@observable isShowLoading = false;
@action showLoading = () => {
this.isShowLoading = true
}
@action hideLoading = () => {
this.isShowLoading = false
}
}
// hotelList
class HotelList {
@observable hotels = []
@action addHotels = (hotels) => {
this.hotels = [...toJS(this.hotels), ...hotels];
}
}
// hotelListAction
class HotelListAction {
fetchHotelList = flow(function *() {
const {
globalStatus,
hotelList
} = this.rootStore
globalStatus.showLoading();
try {
const res = yield fetch('/hoteList', params);
hotelList.addHotels(res.hotels);
} catch (err) {
} finally {
globalStatus.hideLoading();
}
}).bind(this)
}
细粒度的渲染是高效的
observer 可以给组件增加订阅功能,一旦收到数据变化的通知就会将组件重新渲染,从而做到更细粒度的更新,这是 redux 和 react 很难做到的,因为 react 中组件重新渲染基本是依赖于 setState 和接收到新的 props,子组件的渲染几乎一定会伴随着父组件的渲染。
也许很多人没有注意到,mobx-react 中还提供了一个 Observer 组件,这个组件接收一个 render 方法或者 render props。
const App = () => <h1>hello, world</h1>;
<Observer>{() => <App />}</Observer>
<Observer render={() => <App />} />
也许你要问这个和 observer 有什么区别?还写的更加复杂了,下面这个例子对比起来会比较明显。
import { observer, Observer, observable } from 'mobx-react'
const App = observer(
(props) => <h1>hello, {props.name}</h1>
)
const Header = (props) => <h1>this is header</h1>
const Footer = (props) => <h1>this is footer</h1>
const Container = observer(
(props) => {
return (
<>
<Header />
<App name={props.person.name} />
<Footer />
</>
)
}
)
const person = observable({name: "gyyin"});
render(<Container person={person} />, document.getElementById("app"));
person.name = "world";
上面这个代码,Container 组件监听到 person.name 改变的时候会重新渲染,这样就导致了原本不需要重新渲染的 Header 和 Footer 也跟着渲染了,如果使用 Observer 就可以做到更细粒度的渲染。
const App = (props) => <h1>hello, {props.name}</h1>
const Header = (props) => <h1>this is header</h1>
const Footer = (props) => <h1>this is footer</h1>
const Container = (props) => {
return (
<>
<Header />
<Observer render={
() => <App name={props.person.name} />
}>
<Footer />
</>
)
}
const person = observable({name: "gyyin"});
render(<Container person={person} />, document.getElementById("app"));
person.name = "world";
如果在 Header 和 Footer 里面做 console.log,你会发现只有被 Observer 包裹的 App 组件进行了重新渲染,由于 Container 没有订阅数据变化,所以也不会重新渲染。
但如果不是对性能有极致的追求,observer 已经足够了,大量的 Observer 会花费你很多精力来管理渲染问题。
本文如有错误之处,希望大家能够指出,一起讨论。
参考链接: