0%

mobx学习总结

一、Mobx 解决的问题

传统 React 使用的数据管理库为 Redux。Redux 要解决的问题是统一数据流,数据流完全可控并可追踪。要实现该目标,便需要进行相关的约束。Redux 由此引出了 dispatch action reducer 等概念,对 state 的概念进行强约束。然而对于一些项目来说,太过强,便失去了灵活性。Mobx 便是来填补此空缺的。

这里对 Redux 和 Mobx 进行简单的对比:

  1. Redux 的编程范式是函数式的而 Mobx 是面向对象的;

  2. 因此数据上来说 Redux 理想的是 immutable 的,每次都返回一个新的数据,而 Mobx 从始至终都是一份引用。因此 Redux 是支持数据回溯的;

  3. 然而和 Redux 相比,使用 Mobx 的组件可以做到精确更新,这一点得益于 Mobx 的 observable;对应的,Redux 是用 dispath 进行广播,通过 Provider 和 connect 来比对前后差别控制更新粒度,有时需要自己写 SCU;Mobx 更加精细一点。

二、 Mobx 核心概念

image

Mobx 的核心原理是通过 action 触发 state 的变化,进而触发 state 的衍生对象(computed value & Reactions)。

State

在 Mobx 中,State 就对应业务的最原始状态,通过 observable 方法,可以使这些状态变得可观察。

通常支持被 observable 的类型有三个,分别是 Object, Array, Map;对于原始类型,可以使用 Obserable.box。

值得注意的一点是,当某一数据被 observable 包装后,他返回的其实是被 observable 包装后的类型。

1
2
3
4
5
6

const Mobx = require("mobx");
const { observable, autorun } = Mobx;
const obArray = observable([1, 2, 3]);
console.log("ob is Array:", Array.isArray(obArray));
console.log("ob:", obArray);

控制台输出为:

1
2
3

ob is Array: false
ob: ObservableArray {}

对于该问题,解决方法也很简单,可以通过 Mobx 原始提供的 observable.toJS()转换成 JS 再判断,或者直接使用 Mobx 原生提供的 APIisObservableArray 进行判断。

computed

Mobx 中 state 的设计原则和 redux 有一点是相同的,那就是尽可能保证 state 足够小,足够原子。这样设计的原则不言而喻,无论是维护性还是性能。那么对于依赖 state 的数据而衍生出的数据,可以使用 computed。

简而言之,你有一个值,该值的结果依赖于 state,并且该值也需要被 observable,那么就使用 computed。

通常应该尽可能的使用计算属性,并且由于其函数式的特点,可以最大化优化性能。如果计算属性依赖的 state 没改变,或者该计算值没有被其他计算值或响应(reaction)使用,computed 便不会运行。在这种情况下,computed 处于暂停状态,此时若该计算属性不再被 observable。那么其便会被 Mobx 垃圾回收。

简单介绍 computed 的一个使用场景

假如你观察了一个数组,你想根据数组的长度变化作出反应,在不使用 computed 时代码是这样的

1
2
3
4
5
6
7
8
9
10

const Mobx = require("mobx");
const { observable, autorun, computed } = Mobx;
var numbers = observable([1, 2, 3]);
autorun(() => console.log(numbers.length));
// 输出 '3'
numbers.push(4);
// 输出 '4'
numbers[0] = 0;
// 输出 '4'

最后一行其实只是改了数组中的一个值,但是也触发了 autorun 的执行。此时如果用 computed 便会解决该问题。

1
2
3
4
5
6
7
8
9
10

const Mobx = require("mobx");
const { observable, autorun, computed } = Mobx;
var numbers = observable([1, 2, 3]);
var sum = computed(() => numbers.length);
autorun(() => console.log(sum.get()));
// 输出 '3'
numbers.push(4);
// 输出 '4'
numbers[0] = 1;

autorun

另一个响应 state 的 api 便是 autorun。和 computed 类似,每当依赖的值改变时,其都会改变。不同的是,autorun 没有了 computed 的优化(当然,依赖值未改变的情况下也不会重新运行,但不会被自动回收)。因此在使用场景来说,autorun 通常用来执行一些有副作用的。例如打印日志,更新 UI 等等。

action

在 redux 中,唯一可以更改 state 的途径便是 dispatch 一个 action。这种约束性带来的一个好处是可维护性。整个 state 只要改变必定是通过 action 触发的,对此只要找到 reducer 中对应的 action 便能找到影响数据改变的原因。强约束性是好的,但是 Redux 要达到约束性的目的,似乎要写许多样板代码,虽说有许多库都在解决该问题,然而 Mobx 从根本上来说会更加优雅。

首先 Mobx 并不强制所有 state 的改变必须通过 action 来改变,这主要适用于一些较小的项目。对于较大型的,需要多人合作的项目来说,可以使用 Mobx 提供的 api configure 来强制。

1
Mobx.configure({enforceActions: true})

其原理也很简单

1
2
3
4
5
6
7

function configure(options){
    if (options.enforceActions !== undefined) {
        globalState.enforceActions = !!options.enforceActions
        globalState.allowStateChanges = !options.enforceActions
    }
}

通过改变全局的 strictMode 以及 allowStateChanges 属性的方式来实现强制使用 action。

三、Mobx 异步处理

和 Redux 不同的是,Mobx 在异步处理上并不复杂,不需要引入额外的类似 redux-thunk、redux-saga 这样的库。

唯一需要注意的是,在严格模式下,对于异步 action 里的回调,若该回调也要修改 observable 的值,那么

该回调也需要绑定 action。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

const Mobx = require("mobx");
Mobx.configure({ enforceActions: true });
const { observable, autorun, computed, extendObservable, action } = Mobx;
class Store {
  @observable a = 123;

  @action
  changeA() {
    this.a = 0;
    setTimeout(this.changeB, 1000);
  }
  @action.bound
  changeB() {
    this.a = 1000;
  }
}
var s = new Store();
autorun(() => console.log(s.a));
s.changeA();

这里用了 action.bound 语法糖,目的是为了解决 javascript 作用域问题。

另外一种更简单的写法是直接包装 action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Mobx = require("mobx");
Mobx.configure({ enforceActions: true });
const { observable, autorun, computed, extendObservable, action } = Mobx;
class Store {
  @observable a = 123;
  @action
  changeA() {
    this.a = 0;
    setTimeout(action('changeB',()=>{
      this.a = 1000;
    }), 1000);
  }
}
var s = new Store();
autorun(() => console.log(s.a));
s.changeA();

如果不想到处写 action,可以使用 Mobx 提供的工具函数 runInAction 来简化操作。

1
2
3
4
5
6
7
8
9
10
11
12
...
 @action
  changeA() {
    this.a = 0;
    setTimeout(
      runInAction(() => {
        this.a = 1000;
      }),
      1000
    );
  }
...

通过该工具函数,可以将所有对 observable 值的操作放在一个回调里,而不是命名各种各样的 action。

最后,Mobx 提供的一个工具函数,其原理 redux-saga,使用 ES6 的 generator 来实现异步操作,可以彻底摆脱 action 的干扰。

1
2
3
4
5
6
@asyncAction
  changeA() {
    this.a = 0;
    const data = yield Promise.resolve(1)
    this.a = data;
  }

四、Mobx 原理分析

autorun

Mobx 的核心就是通过 observable 观察某一个变量,当该变量产生变化时,对应的 autorun 内的回调函数就会发生变化。

1
2
3
4
5
6
7
8
const Mobx = require("mobx");
const { observable, autorun } = Mobx;
const ob = observable({ a: 1, b: 1 });
autorun(() => {
  console.log("ob.b:", ob.b);
});

ob.b = 2;

执行该代码会发现,log 了两遍 ob.b 的值。其实从这个就能猜到,Mobx 是通过代理变量的 getter 和 setter 来实现的变量更新功能。首先先代理变量的 getter 函数,然后通过预执行一遍 autorun 中回调,从而触发 getter 函数,来实现观察值的收集,依次来代理 setter。之后只要 setter 触发便执行收集好的回调就 ok 了。
具体源码如下:

1
2
3
4
5
6
7
8
function autorun(view, opts){
reaction = new Reaction(name, function () {
this.track(reactionRunner);
}, opts.onError);
function reactionRunner() {
view(reaction);
}
}

autorun 的核心就是这一段,这里 view 就是 autorun 里的回调函数。具体到 track 函数,比较关键到代码是:

1
2
3
Reaction.prototype.track = function (fn) {
var result = trackDerivedFunction(this, fn, undefined);
}

trackDerivedFunction 函数中会执行 autorun 里的回调函数,紧接着会触发 observable 中代理的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
function generateObservablePropConfig(propName) {
return (observablePropertyConfigs[propName] ||
(observablePropertyConfigs[propName] = {
configurable: true,
enumerable: true,
get: function () {
return this.$mobx.read(this, propName);
},
set: function (v) {
this.$mobx.write(this, propName, v);
}
}));
}

在 get 中会将回调与其绑定,之后更改了 observable 中的值时,都会触发这里的 set,然后随即触发绑定的函数。

五、Mobx 的一些坑

通过 autorun 的实现原理可以发现,会出现很多我们想象中应该触发,但是没有触发的场景,例如:

  1. 无法收集新增的属性
1
2
3
4
5
6
7
8
9
const Mobx = require("mobx");
const { observable, autorun } = Mobx;
let ob = observable({ a: 1, b: 1 });
autorun(() => {
if(ob.c){
console.log("ob.c:", ob.c);
}
});
ob.c = 1

对于该问题,可以通过 extendObservable(target, props)方法来实现。

1
2
3
4
5
6
7
8
9
10
const Mobx = require("mobx");
const { observable, autorun, computed, extendObservable } = Mobx;
var numbers = observable({ a: 1, b: 2 });
extendObservable(numbers, { c: 1 });
autorun(() => console.log(numbers.c));
numbers.c = 3;

// 1

// 3

extendObservable 该 API 会可以为对象新增加 observal 属性。

当然,如果你对变量的 entry 增删非常关心,应该使用 Map 数据结构而不是 Object。

  1. 回调函数若依赖外部环境,则无法进行收集
1
2
3
4
5
6
7
8
9
10
11
const Mobx = require("mobx");
const { observable, autorun } = Mobx;
let ob = observable({ a: 1, b: 1 });
let x = 0;
autorun(() => {
if(x == 1){
console.log("ob.c:", ob.b);
}
});
x = 1;
ob.b = 2;

很好理解,autorun 的回调函数在预执行的时候无法到达 ob.b 那一行代码,所以收集不到。