[关闭]
@gyyin 2020-02-09T14:04:12.000000Z 字数 4802 阅读 393

深入 MobX 实践及原理

慕课专栏


前言

mobx 是另一种 React 状态管理方案,和 Redux 不一样的是,mobx 并非是 immutable 的库,走的是和 Vue 理念更相符的 Reactive 路线,在写法上也更加简洁。

图片来自 mobx 中文文档:

image_1e0hnspgn1d6pomqhrc16g7somp.png-89.1kB

这篇文章会简单介绍一下 mobx 用法,带你深入探索 mobx 的最佳实践以及原理实现。如果对 mobx 详细的用法感兴趣,那么可以参考 mobx 中文文档:mobx 中文文档

mobx

store

mobx 的 store 组织方式更偏向于传统的面向对象,一般推荐使用 class 来定义 store 的结构。
不管是 state 还是 action,它们都是 store 类上面的属性,修改 state 也可以直接修改,这点儿和 Redux 大不一样。

  1. // store.js
  2. class Store {
  3. @observable count = 0;
  4. @action changeCount(count) {
  5. this.count = count;
  6. }
  7. }
  8. export default new Store();

observable

observable 是 mobx 提供的一个用于观察对象属性的 API。

  1. import { observable } from 'mobx';
  2. class Store {
  3. @observable count = 0;
  4. }

observable 常配合装饰器语法一起使用,如果你的环境不支持装饰器,那也可以退出到 ES5 的方式来使用。

  1. import {
  2. decorate,
  3. observable
  4. } from 'mobx';
  5. class Store {
  6. count = 0;
  7. }
  8. decorate(Store, {
  9. count: observable
  10. })

如果不在 class 中使用,就可以直接包裹着对象。

  1. const person = observable({
  2. count: 0
  3. });
  4. person.count = 1;

computed

如果你用过 Vue,对 computed 一定不会陌生。顾名思义,computed 意思就是计算属性,这个计算属性是根据现有的状态和其他值计算衍生得来的。
常配合 @computed 装饰器来使用,一旦依赖的可观察属性发生变化,就会重新计算结果。
下面这个例子中的 isEmpty 属性就是根据可观察属性 todos 衍生而来的:

  1. import { observable, computed } from 'mobx';
  2. class Store {
  3. @observable todos = [];
  4. @computed get isEmpty() {
  5. return this.todos.length === 0;
  6. }
  7. }

autorun

autorun 是一个响应函数,它接收一个函数作为参数,这个函数会被立即执行一次,然后每次它的依赖关系改变时会再次被触发。常被用于打印日志或者更新 UI 的场景。

  1. const store = observable({
  2. count: 0
  3. });
  4. autorun(function log() {
  5. console.log(store.count);
  6. });
  7. store.count = 1;

这段程序会先后输出0和1,这是因为 autorun 接收的 log 函数立即执行一次输出了0,然后 count 属性被修改了,log 函数又被执行了一遍。
为什么 autorun 里面的 log 函数会被立即执行一次呢?这是为了从 log 函数中收集依赖,比如上面的 store.count,一旦依赖发生了变化,就会再次执行一下 log 函数。

reaction

reaction 则是和 autorun 功能类似,但是 autorun 会立即执行一次,而 reaction 不会,使用 reaction 可以在监听到指定数据变化的时候执行一些操作,有利于和副作用代码解耦。

  1. // 当todos改变的时候将其存入缓存
  2. reaction(
  3. () => toJS(this.todos),
  4. (todos) => localStorage.setItem('mobx-react-todomvc-todos', JSON.stringify({ todos }))
  5. )

action

在 Redux 中也有 action 的概念,一般是用来发送通知修改 state 的。mobx 中的 action 也是用在修改 state 中的,但在 mobx 中即使不使用 action 也可以做到修改 state,autorun 依然能够响应。
action 接收一个函数并返回具有同样签名的函数,但是用 transaction、untracked 和 allowStateChanges 包裹起来,尤其是 transaction 的自动应用会产生巨大的性能收益,也利于打印相关修改信息。

  1. class Store {
  2. @observable count = 0;
  3. @action changeCount(count) {
  4. this.count = count;
  5. }
  6. }

observer

MobX 本身并未依赖任何框架,mobx-react 则是一个将 MobX 和 React 绑定起来的库。
observer 是一个高阶组件,可以将你的 React 组件变成一个响应式组件。只要传来的 count 发生了变化,那么组件就会重新刷新,本质上是对 render 做了拦截。

  1. import { observer } from 'mobx-react';
  2. @observer
  3. class App extends React.Component {
  4. render() {
  5. return <h1>{ props.count }</h1>
  6. }
  7. }

实践

在简单地介绍完了 MobX 的主要用法后,那么如何在项目中更好地使用 MobX 呢?

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 会自动更新。

  1. class TodoStore {
  2. @observable todos = []
  3. @computed get completedCount() {
  4. return (this.todos.filter(todo => todo.isCompleted) || []).length
  5. }
  6. }

管理局部状态

在 react 中,我们更新状态需要使用 setState,但是 setState 后并不能立马拿到更新后的 state,虽然 setState 提供了一个回调函数,我们也可以用 Promise 来包一层,但终究还是个异步的方式。
在 mobx 中,我们可以直接在 react 的 class 里面用 observable 声明属性来代替 state,这样可以立马拿到更新后的值,而且 observer 会做一些优化,避免了频繁 render。

  1. @observer
  2. class App extends React.Component {
  3. @observable count = 0;
  4. constructor(props) {
  5. super(props);
  6. }
  7. @action
  8. componentDidMount() {
  9. this.count = 1;
  10. this.count = 2;
  11. this.count = 3;
  12. }
  13. render() {
  14. return <h1>{this.count}</h1>
  15. }
  16. }

拆分 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 的结构。

  1. - stores
  2. - actions
  3. - hotelListAction.js
  4. - models
  5. - globalStatus.js
  6. - hotelList.js
  7. - index.js
  8. // globalStatus
  9. class GlobalStatus {
  10. @observable isShowLoading = false;
  11. @action showLoading = () => {
  12. this.isShowLoading = true
  13. }
  14. @action hideLoading = () => {
  15. this.isShowLoading = false
  16. }
  17. }
  18. // hotelList
  19. class HotelList {
  20. @observable hotels = []
  21. @action addHotels = (hotels) => {
  22. this.hotels = [...toJS(this.hotels), ...hotels];
  23. }
  24. }
  25. // hotelListAction
  26. class HotelListAction {
  27. fetchHotelList = flow(function *() {
  28. const {
  29. globalStatus,
  30. hotelList
  31. } = this.rootStore
  32. globalStatus.showLoading();
  33. try {
  34. const res = yield fetch('/hoteList', params);
  35. hotelList.addHotels(res.hotels);
  36. } catch (err) {
  37. } finally {
  38. globalStatus.hideLoading();
  39. }
  40. }).bind(this)
  41. }

image_1e0id5tot7flljg4mc1srffqb1j.png-33.1kB

原理

在前面的文章中,我们已经介绍过 Object.definePropertyProxy,他们可以通过拦截 getter/setter 的形式来实现数据响应。
在 MobX5 以前底层实现都用的是 Object.defineProperty,MobX5 使用了新的 Proxy,接下来将使用 Proxy 带你实现简单的 MobX。
在开始实现具体的 API 之前,先来梳理一下 MobX 该如何实现?
MobX 的核心原理就是拦截 observable 数据的 getter/setter,顾名思义,getter 就是指你去访问这个数据的时候,所以常常是用在 autorun 里面。
autorun 里面的函数会立即执行一次就是为了知道这个函数依赖了哪些数据,这个函数会订阅这些数据的变化,这一步叫做“依赖收集”。
setter 则是你对数据重新赋值的时候,会通知所有订阅过的函数执行,这有点儿像发布-订阅模式。
所以实现 MobX 的关键在于怎么进行依赖收集,可以考虑第一次执行 autorun 里面函数之前,先将这个函数保存起来,等这个函数执行的时候,在 getter 中将这个函数放到一个数组中,下次 setter 的时候从数组中取出来执行。

image_1e0k8o8ug6aj1sviir1b6a1lfn9.png-18.8kB

observable

先来整理一下 observable 的作用,它是一个装饰器、将数据变成响应式、触发 autorun 等等。

  1. const observable = (target, name, descriptor) => {
  2. const v = descriptor.initializer.call(this);
  3. const p = new Proxy(v, {
  4. })
  5. }

mobx-react

推荐阅读

  1. Mobx 项目实践
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注