@gyyin
2020-02-09T14:04:12.000000Z
字数 4802
阅读 393
慕课专栏
mobx 是另一种 React 状态管理方案,和 Redux 不一样的是,mobx 并非是 immutable 的库,走的是和 Vue 理念更相符的 Reactive 路线,在写法上也更加简洁。
图片来自 mobx 中文文档:
这篇文章会简单介绍一下 mobx 用法,带你深入探索 mobx 的最佳实践以及原理实现。如果对 mobx 详细的用法感兴趣,那么可以参考 mobx 中文文档:mobx 中文文档
mobx 的 store 组织方式更偏向于传统的面向对象,一般推荐使用 class 来定义 store 的结构。
不管是 state 还是 action,它们都是 store 类上面的属性,修改 state 也可以直接修改,这点儿和 Redux 大不一样。
// store.js
class Store {
@observable count = 0;
@action changeCount(count) {
this.count = count;
}
}
export default new Store();
observable 是 mobx 提供的一个用于观察对象属性的 API。
import { observable } from 'mobx';
class Store {
@observable count = 0;
}
observable 常配合装饰器语法一起使用,如果你的环境不支持装饰器,那也可以退出到 ES5 的方式来使用。
import {
decorate,
observable
} from 'mobx';
class Store {
count = 0;
}
decorate(Store, {
count: observable
})
如果不在 class 中使用,就可以直接包裹着对象。
const person = observable({
count: 0
});
person.count = 1;
如果你用过 Vue,对 computed 一定不会陌生。顾名思义,computed 意思就是计算属性,这个计算属性是根据现有的状态和其他值计算衍生得来的。
常配合 @computed
装饰器来使用,一旦依赖的可观察属性发生变化,就会重新计算结果。
下面这个例子中的 isEmpty
属性就是根据可观察属性 todos
衍生而来的:
import { observable, computed } from 'mobx';
class Store {
@observable todos = [];
@computed get isEmpty() {
return this.todos.length === 0;
}
}
autorun 是一个响应函数,它接收一个函数作为参数,这个函数会被立即执行一次,然后每次它的依赖关系改变时会再次被触发。常被用于打印日志或者更新 UI 的场景。
const store = observable({
count: 0
});
autorun(function log() {
console.log(store.count);
});
store.count = 1;
这段程序会先后输出0和1,这是因为 autorun 接收的 log 函数立即执行一次输出了0,然后 count 属性被修改了,log 函数又被执行了一遍。
为什么 autorun 里面的 log 函数会被立即执行一次呢?这是为了从 log 函数中收集依赖,比如上面的 store.count
,一旦依赖发生了变化,就会再次执行一下 log 函数。
reaction 则是和 autorun 功能类似,但是 autorun 会立即执行一次,而 reaction 不会,使用 reaction 可以在监听到指定数据变化的时候执行一些操作,有利于和副作用代码解耦。
// 当todos改变的时候将其存入缓存
reaction(
() => toJS(this.todos),
(todos) => localStorage.setItem('mobx-react-todomvc-todos', JSON.stringify({ todos }))
)
在 Redux 中也有 action 的概念,一般是用来发送通知修改 state 的。mobx 中的 action 也是用在修改 state 中的,但在 mobx 中即使不使用 action 也可以做到修改 state,autorun 依然能够响应。
action 接收一个函数并返回具有同样签名的函数,但是用 transaction、untracked 和 allowStateChanges 包裹起来,尤其是 transaction 的自动应用会产生巨大的性能收益,也利于打印相关修改信息。
class Store {
@observable count = 0;
@action changeCount(count) {
this.count = count;
}
}
MobX 本身并未依赖任何框架,mobx-react 则是一个将 MobX 和 React 绑定起来的库。
observer 是一个高阶组件,可以将你的 React 组件变成一个响应式组件。只要传来的 count 发生了变化,那么组件就会重新刷新,本质上是对 render 做了拦截。
import { observer } from 'mobx-react';
@observer
class App extends React.Component {
render() {
return <h1>{ props.count }</h1>
}
}
在简单地介绍完了 MobX 的主要用法后,那么如何在项目中更好地使用 MobX 呢?
想像一下,在 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
}
}
在 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>
}
}
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
- models
- 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)
}
在前面的文章中,我们已经介绍过 Object.defineProperty
和 Proxy
,他们可以通过拦截 getter/setter
的形式来实现数据响应。
在 MobX5 以前底层实现都用的是 Object.defineProperty
,MobX5 使用了新的 Proxy
,接下来将使用 Proxy
带你实现简单的 MobX。
在开始实现具体的 API 之前,先来梳理一下 MobX 该如何实现?
MobX 的核心原理就是拦截 observable 数据的 getter/setter
,顾名思义,getter
就是指你去访问这个数据的时候,所以常常是用在 autorun
里面。
autorun 里面的函数会立即执行一次就是为了知道这个函数依赖了哪些数据,这个函数会订阅这些数据的变化,这一步叫做“依赖收集”。
而 setter
则是你对数据重新赋值的时候,会通知所有订阅过的函数执行,这有点儿像发布-订阅模式。
所以实现 MobX 的关键在于怎么进行依赖收集,可以考虑第一次执行 autorun 里面函数之前,先将这个函数保存起来,等这个函数执行的时候,在 getter
中将这个函数放到一个数组中,下次 setter
的时候从数组中取出来执行。
先来整理一下 observable 的作用,它是一个装饰器、将数据变成响应式、触发 autorun 等等。
const observable = (target, name, descriptor) => {
const v = descriptor.initializer.call(this);
const p = new Proxy(v, {
})
}