从《MobX Quick Start Guide》出发快速掌握MobX的使用

2,385 阅读11分钟

前言

最近在学习MobX,感觉看官网的文档有些细节理解不会很清楚,所以刷完了官网推荐的《MobX Quick Start Guide》一书,从头梳理了思路写下了这篇文章,希望可以给想要快速了解MobX的小伙伴们提供一些些帮助,如果文章中有描述不清的点或是错误,欢迎大家在评论区跟我交流讨论。

这篇文章从MobX是什么展开,再通过MobX与Redux的对比让小伙伴们了解MobX的一些优势是什么,然后正式进入MobX的世界,去了解以下内容:

  • MobX的运行机制是什么样的
  • MobX有哪些核心概念
  • MobX如何与React结合使用

最后会用一个简单的demo来向大家介绍在项目中使用MobX的思路。接下来,我们就赶快开始吧~

MobX是什么

相信大家在项目开发时一定会用到一些状态管理库,来让状态管理变得更加简单可控,比如说React项目里大家常用的Redux,Vue项目里大家常用的Vuex。而我们的主角MobX也是一个状态管理的工具,它最大的特点是简单可扩展

说它简单就不得不提到MobX哲学——任何源自应用状态的东西都应该自动的获得。比如说实际项目经常可能有这样的需求:状态C是根据状态A和状态B计算得到的,所以状态A/B发生改变时,状态C也应该重新计算,在MobX里应用它的computed,当状态A/B发生改变时,它会自动帮我们更新状态C,嘿嘿,是不是很方便呢。

看到这里可能有小伙伴会吐槽Vuex的getter也可以实现这样的功能啊,哈哈,我们先不着急,先继续往下看啥后面会逐步揭开MobX的优势所在的。

MobX与Redux的对比

单向数据流

单向数据流:store管理所有的state变化以及在state发生改变时通知UI和其他观察者

Redux和MobX都是采用的单向数据流,但它们的实现机制是不同的,Redux依赖不可变state快照和两个state快照之间的引用比较来检查是否发生改变,相反,MobX在可变状态下蓬勃发展,并使用粒度(Atoms)通知系统来跟踪状态变化。

开发难度低

Redux的API遵循函数编程风格,对初入门只具备面向对象知识的新人来说有一点理解难度,MobX使用语义丰富的响应式编程风格,对面向对象匹配更加简略的API语法,大大降低了学习成本,同时MobX的集成度也比Redux稍高,避免了让开发者引入众多零散的第三方库。

因为Redux的reducer是纯函数,不能添加副作用,所以中间件是Redux里唯一可以执行副作用的地方,因此Redux的使用经常需要添加许多第三方的中间件,比如常见的用redux-thunk来支持异步操作等。而MobX集成度较高,异步操作等开箱即用,不用添加额外的三方库。

开发代码量少

Redux拥有reducer,actions,store等众多概念,每增加一个状态都要同步更新这些位置,样板代码较多。Mobx只要在store中更新即可,代码编写量上大大少于Redux。

渲染性能好

在react中合理编写shouldComponentUpdate可以避免不必要的重渲染,提升页面性能,但是如果数据层次在复杂的话实现这个方法并非易事,而MobX精确描述哪些组件是需要重渲染的,哪些不需要重渲染,通过合理的组织组件层级和数据结构位置,可以轻易的将视图重渲染控制在最小的限制范围之内,从而影响页面性能。

嘿嘿通过上面的对比是不是觉得MobX看上去还挺不错的样子,简单方便上手快,性能还好,那还犹豫什么,接着往下读,让我们一起进入MobX的世界吧~

MobX的运行机制

我们一起来看一下下面这张模型图,这张模型图描述了单向数据流的思想,也就是说状态的改变会对应页面UI的改变,页面UI上用户事件的操作会触发Action的操作再去更新状态。

但是大家有没有发现一个小问题,就是这张模型图上体现不出副作用(比如网络请求,日志等)该如何处理,那么让我们来更新一下模型图。

这次的模型图上状态的改变不仅要通知UI还要通知副作用的处理器,副作用的处理器收到通知后会执行副作用,然后将副作用产生的改变告诉到actions去更新状态。

哈哈,是不是觉得UI和副作用处理器干的事情很像呢,它们都会接收状态的通知,最后通过Actions来变更状态,所以我们可以认为它们都是观察者,它们在观察状态,一旦状态发生了改变,它们会做出一些相应的响应。

嘿嘿,其实MobX也是这种思想,它的状态叫observables——是可以被观察到变化的对象,它的UI和副作用就是observers——是观察者,它的actions还叫actions哈~~当observables发生改变时会发送消息通知到它的observers,然后observers可以通过actions做一些些处理再通知observables的更新。

上面几张图跟大家分享了MobX是怎么运转的,接下来我们了解一些MobX的核心概念,把MobX用起来!

MobX的核心概念

嘿嘿,因为这部分都是概念性的内容,我们就用极简的方式快速过一遍核心概念和常用的一些API等,后面demo里面讲解怎么用起来,想要了解更多API细节的小伙伴可以参考一下文档撒~

observables

  1. 概念: 创建一个可观察对象,它可以跟踪发生在它的任何属性上的变化。
  2. API:
  • observables:只能转换对象,数组,map这些
  • observable.box:可以转换 JavaScript primitives (number, string, boolean, null, undefined), functions, or for class- instances (objects with prototypes)
  • extendObservable(target, object, decorators):它允许在运行时混合其他属性,并使它们成为可观察的。observable. map也可以动态的扩展 observable properties,但是它不能动态扩展actions和 computed-properties。
  1. 装饰器语法
  • shallow:只观察第一层数据
  • ref:不需要观察属性变化(属性是只读的),频繁变更引用时使用ref
  • struct:对象每次更新都会触发reaction,但是有时只是reference更新了实际属性内容没变,这就是struct存在的意义。struct会基于property做深比较。

actions

  1. 概念: 接受一个函数,该函数用于修改observables的状态,将在调用操作时被调用。
  2. 异步actions
  • runInAction: runInAction(fn) === action(fn)() 因为对observable的修改必须在action函数内,而async函数是Generator函数的语法糖,await后面的操作放到了回调函数里,因为它不直接在action函数里了,所以我们用runInAction套上一层,保证await后面的操作还在action里。
import { action, observable, configure, runInAction } from 'mobx';
configure({ enforceActions: 'always'});

class ShoppingCart {
    @observable asyncState = '';
    @observable.shallow items = [];
    @action async submit() {
        this.asyncState = 'pending';
        const response = await this.purchaseItems(this.items);
        runInAction(() = >{
            this.asyncState = 'completed';
        });
    }
    purchaseItems(items) {
        /* ... */
        return Promise.resolve({});
    }
  }
}
  • flow:解决到处套runInAction的问题 如果有很多个await就要频繁的用runInAction,代码比较不优雅,所以这时可以考虑用flow
import { observable, flow, configure } from 'mobx';
configure({ enforceActions: 'strict' });
class AuthStore {
    @observable loginState = '';
    login = flow(function*(username, password) {
        this.loginState = 'pending';
        yield this.initializeEnvironment();
        this.loginState = 'initialized';
        yield this.serverLogin(username, password);
        this.loginState = 'completed';
        yield this.sendAnalytics();
        this.loginState = 'reported';
        yield this.delay(3000);
    });
} 
new AuthStore().login();
  1. 优点:
  • 可读性更好:用actions封装操作会更具语义化
  • 性能的大大提升:它会将多次的修改作为一次原子事务,这也可以减少过多通知的噪音
  1. 深入理解actions的优势
action = untracked(transaction(allowStateChanges(true,\<mutating-function>)))
  • untracked: 防止在mutating function中跟踪观察对象(也称为创建新的 observable-observer 关系)
  • transaction: 批处理通知,在相同的观察对象上强制通知,然后在操作结束时分发最小的通知集
  • allowStateChanges: 这确保了状态更改确实发生在可观察表上,并且它们将获得新值

这样组合actions的好处是:

  1. 减少过度的通知
  2. 通过批量处理最少的一组通知来提高效率
  3. 尽量减少副作用的执行,对于在一个动作中多次改变的可观察对象
  4. action通过包装细节,让代码语义化更好,可读性更强

Derivations

任何源自状态并且不会再有任何进一步的相互作用的东西就是衍生。衍生主要有两种,一是computed,二是observers,接下来我们来分别介绍一下它们~

computed

  1. 概念:可以根据现有的状态或其它计算值衍生出的值。
  2. 优点:它会缓存上一次的计算值,如果依赖项发生更改它才会重新计算,如果计算结果与之前缓存结果一致也不会触发通知。而且没人用它他会被垃圾回收机制回收掉。
  3. 装饰器语法
  • struct:diff时不是比较reference而是深入比较内容

observers(reactions)

  1. 概念:观察员,也叫做reactions,包括副作用处理函数和UI。reactions是对状态变化作出响应的动作。
  2. 类型
  • autorun: 依赖项发生变化时自动执行,使用其返回值函数可以将其注销掉
  • reaction: 精确控制一些依赖的变化满足条件时才做出响应
    reaction(tracker-function, effect-function): disposer-function
    
    • 参数:
      • tracker-function:() => data,追踪所有observables,只要有observable发生变化它就会被执行。它应该返回一个值,用于将它与上一次运行的tracker-function进行比较。如果两次的值不同,effect-function就会被触发。
      • effect-function:(data) => {},在effect里使用的任何observable是不会被追踪的
      • disposer-function:是reaction的返回值函数,调用它可以销毁reaction
  • when: 只有在满足条件时执行effect-function,并在满足条件后自动处理副作用,是一次性的副作用
    when(predicate-function, effect-function): disposer-function
    
    • 参数:
      • predicate-function: () => boolean
      • effect-function: ()=>{}
  1. 选择恰当observers的方法

判断副作用是否需要执行多次,如果不是只需要执行一次即可则使用when;如果需要执行多次,考虑一下是不是每次依赖更新都要执行,还是需要对依赖的变化进行一些处理满足条件再做,如果是每次依赖更新都做就使用autorun,需要对依赖做进一步处理再决定做不做后续的处理则用reaction。

MobX与React的结合使用

工具库

  • mobx
  • react-mobx

操作思想

  1. 通过react-mobx提供的Provider将store从根组件传下去
  2. 在需要使用store的组件用react-mobx的inject注入

具体操作

// index.js
import { store } from './BookStore';
import { preferences } from 'PreferencesStore;
import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'mobx-react';
ReactDOM.render(
  <Provider store={store} userPreferences={preferences}>
    <App />
  </Provider>,
  document.getElementById('root')
);
// component.js
@inject('userPreferences')
@observer
class PreferencesViewer extends React.Component {
  render() {
  const { userPreferences } = this.props;
    /* ... */
  }
}

MobX demo的快速上手

这里我们用todolist的例子来展示如何快速上手MobX,具体介绍mobx部分代码的设计思路,完整的代码实现可以参考这个Demo

1. 确定state(observable和computed)

让我们根据UI设计图来看一下todoList需要哪些state:

  • observable
    • inputText: input框的输入内容
    • filter:筛选条件(all/completed/unCompleted)
    • todoList:todo的列表数据
    • cid:todo的唯一标识符
  • computed
    • showTodoList: 展示在页面的todoList

2. 确定交互的action

  • addTodo:增加todo
  • deleteTodo:删除todo
  • changeInput:改变添加输入框的值
  • changeTodoStatus:改变todo的完成状态
  • changeTodoText:改变todo的内容
  • changeFilter:改变筛选条件

3. 确定副作用reaction

  • logger: todoList变化时打印日志
import { configure, observable, computed, action } from 'mobx';
configure({ enforceActions: 'always' });

class TodoListData {
  @observable inputText = '';
  @observable todoList = [];
  @observable filter = 'All';
  @observable cid = 0;

  @computed get showTodoList() {
    let showTodoList = this.todoList;
    this.filter === 'Completed' &&
      (showTodoList = this.todoList.filter((todo) => todo.completed));
    this.filter === 'UnCompleted' &&
      (showTodoList = this.todoList.filter((todo) => !todo.completed));
    return showTodoList;
  }

  @action.bound
  addTodo() {
    if (this.inputText) {
      this.todoList.push({
        id: `todo-${this.cid++}`,
        text: this.inputText,
        completed: false,
      });
      this.inputText = '';
    }
  }

  @action.bound
  deleteTodo(id) {
    this.todoList = this.todoList.filter((todo) => todo.id !== id);
  }

  @action.bound
  changeInput(value) {
    this.inputText = value;
  }

  @action.bound
  changeFilter(filter) {
    this.filter = filter;
  }

  @action.bound
  changeTodoStatus(id) {
    this.todoList.find(
      (todo) => todo.id === id && (todo.completed = !todo.completed)
    );
  }

  @action.bound
  changeTodoText(id, value) {
    this.todoList.find((todo) => todo.id === id && (todo.text = value));
  }
}

export default new TodoListData();

总结

Emmmmmm,终于写完了,最后我们来个小回顾吧~本文从MobX是什么展开,再通过MobX与Redux的对比让小伙伴们了解MobX的一些优势是什么,然后正式进入MobX的世界,去了解MobX的运行机制是什么样的、MobX有哪些核心概念、MobX如何与React结合使用,最后还给出了一个小demo让刚上手MobX的小伙伴了解如何设计MobX的代码。就希望对初学MobX的小伙伴有用~如果有什么问题也欢迎大家在评论区留言,我们一起交流,共同进步!最后,厚脸皮的希望看完文章的小伙伴点个赞~~~

参考资料