【前端学习笔记】mobx初认识

https://segmentfault.com/a/1190000015554899

定义

1、React的render是将存储的状态转化为树状结构来渲染组件的方法; Mobx是用来存储和更新状态的工具;

2、React使用的方法是采用虚拟Dom来进行view视图的缓存,来减少繁琐的Dom更新 而Mobx则通过一个虚拟的状态依赖图表来让react组件和应用状态同步化来减少不必要的状态来更新组件的

通过观察者模式对数据做出追踪处理,在对可观察属性的作出变更或者引用的时候,触发其依赖的监听函数。

与其他状态管理工具的区别

1、对比Redux
Redux的编程范式是函数式的而Mobx是面向对象的;因此数据上来说Redux理想的是immutable的,每次都返回一个新的数据,而Mobx从始至终都是一份引用。因此Redux是支持数据回溯的; 然而和Redux相比,使用Mobx的组件可以做到精确更新,这一点得益于Mobx的observable;对应的,Redux是用dispath进行广播,通过Provider和connect来比对前后差别控制更新粒度,有时需要自己写SCU;
Mobx更加精细一点。

2、对比Vuex
component–>dispatch(action)–>mutation–(mutate)–>state–>component
vuex中提出了同步mutation和异步action,现在mobx也无需mutaiton,但借鉴了computed这个纯函数。

相比这两个工具,MobX内置了数据变化监听机制,使得在实际应用的时候一切都是那么的顺其自然。

MobX的数据驱动解构:
action–(update)–>state–(update)–>computed–(trigger)–>reaction

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

核心概念

【前端学习笔记】mobx初认识_第1张图片

observable用法

observable(value)
@observable classProperty = value

Observable 值可以是JS基本数据类型、引用类型、普通对象、类实例、数组和映射。 匹配类型应用了以下转换规则,但可以通过使用调节器进行微调。
例如:

const map = observable.map({ key: "value"});
map.set("key", "new value");

const list = observable([1, 2, 4]);
list[2] = 3;

const person = observable({
    firstName: "Clive Staples",
    lastName: "Lewis"
});
person.firstName = "C.S.";

const temperature = observable.box(20);
temperature.set(25);

如果 value 是ES6的 Map : 会返回一个新的 Observable Map。如果你不只关注某个特定entry的更改,而且对添加或删除其他entry时也做出反应的话,那么 Observable maps 会非常有用
如果 value 是数组,会返回一个 Observable Array。
如果 value 是没有原型的对象,那么对象会被克隆并且所有的属性都会被转换成可观察的。参见 Observable Object。

如果 value 是有原型的对象,JavaSript 原始数据类型或者函数,会返回一个 Boxed Observable。MobX 不会将一个有原型的对象自动转换成可观察的,因为这是它构造函数的职责。在构造函数中使用 extendObservable 或者在类定义中使用 @observable。

如果 value 是有原型的对象,JavaSript 原始数据类型或者函数,observable 会抛出。如果想要为这样的值创建一个独立的可观察引用,请使用 Boxed Observable observable 代替。MobX 不会将一个有原型的对象自动转换成可观察的,因为这是它构造函数的职责。在构造函数中使用 extendObservable 或在类定义上使用 @observable / decorate 。

@observable

@observable 可以在实例字段和属性 getter 上使用。 对于对象的哪部分需要成为可观察的,@observable 提供了细粒度的控制。

import { observable, computed } from "mobx";

class OrderLine {
    @observable price = 0;
    @observable amount = 1;

    @computed get total() {
        return this.price * this.amount;
    }
}

observable对象

observable.object(props, decorators?, options?)

如果把一个普通的 JavaScript 对象传递给 observable 方法,对象的所有属性都将被拷贝至一个克隆对象并将克隆对象转变成可观察的。 (普通对象是指不是使用构造函数创建出来的对象,而是以 Object 作为其原型,或者根本没有原型。) 默认情况下,observable 是递归应用的,所以如果对象的某个值是一个对象或数组,那么该值也将通过 observable 传递。

import {observable, autorun, action} from "mobx";

var person = observable({
    // observable 属性:
    name: "John",
    age: 42,
    showAge: false,

    // 计算属性:
    get labelText() {
        return this.showAge ? `${this.name} (age: ${this.age})` : this.name;
    },

    // 动作:
    setAge(age) {
        this.age = age;
    }
}, {
    setAge: action
});

// 对象属性没有暴露 'observe' 方法,
// 但不用担心, 'mobx.autorun' 功能更加强大
autorun(() => console.log(person.labelText));

person.name = "Dave";
// 输出: 'Dave'

person.setAge(21);
// 等等

当使对象转变成 observable 时,需要记住一些事情:

只有普通的对象可以转变成 observable 。对于非普通对象,构造函数负责初始化 observable 属性。 要么使用 @observable 注解,要么使用 extendObservable 函数。
属性的 getter 会自动转变成衍生属性,就像 @computed 所做的。
observable 是自动递归到整个对象的。在实例化过程中和将来分配给 observable 属性的任何新值的时候。Observable 不会递归到非普通对象中。
这些默认行为能应对95%的场景,但想要更细粒度的控制,比如哪些属性应该转变成可观察的和如何变成可观察的,请参见装饰器。
传入 { deep: false } 作为第三个参数可以禁用属性值的自动转换
传入 { name: “my object” } 为本对象赋予友好的调试名称
[MobX 4 及以下版本] 当通过 observable 传递对象时,只有在把对象转变 observable 时存在的属性才会是可观察的。 稍后添加到对象的属性不会变为可观察的,除非使用 set 或 extendObservable。

observable 数组

observable.object(props, decorators?, options?)

和对象类似,可以使用 observable.array(values?) 或者将数组传给 observable,可以将数组转变为可观察的。 这也是递归的,所以数组中的所有(未来的)值都会是可观察的。

import {observable, autorun} from "mobx";

var todos = observable([
    { title: "Spoil tea", completed: true },
    { title: "Make coffee", completed: false }
]);

autorun(() => {
    console.log("Remaining:", todos
        .filter(todo => !todo.completed)
        .map(todo => todo.title)
        .join(", ")
    );
});
// 输出: 'Remaining: Make coffee'

todos[0].completed = false;
// 输出: 'Remaining: Spoil tea, Make coffee'

todos[2] = { title: 'Take a nap', completed: false };
// 输出: 'Remaining: Spoil tea, Make coffee, Take a nap'

todos.shift();
// 输出: 'Remaining: Make coffee, Take a nap'

除了所有内置函数,observable 数组还可以使用下面的好东西:

intercept(interceptor) - 可以用来在任何变化作用于数组前将其拦截。参见 observe & intercept
observe(listener, fireImmediately? = false) - 监听数组的变化。回调函数将接收表示数组拼接或数组更改的参数,它符合 ES7 提议。它返回一个清理函数以用来停止监听器。
clear() - 从数组中删除所有项。
replace(newItems) - 用新项替换数组中所有已存在的项。
find(predicate: (item, index, array) => boolean, thisArg?) - 基本上等同于 ES7 的 Array.find 提议。
findIndex(predicate: (item, index, array) => boolean, thisArg?) - 基本上等同于 ES7 的 Array.findIndex 提议。
remove(value) - 通过值从数组中移除一个单个的项。如果项被找到并移除的话,返回 true 。
[MobX 4 及以下版本] peek() - 和 slice() 类似, 返回一个有所有值的数组并且数组可以放心的传递给其它库。
不同于 sort 和 reverse 函数的内置实现,observableArray.sort 和 observableArray.reverse 不会改变数组本身,而只是返回一个排序过/反转过的拷贝。在 MobX 5 及以上版本中会出现警告。推荐使用 array.slice().sort() 来替代。

observable 映射

observable.map(values, options?)

observable.map(values?) - 创建一个动态键的 observable 映射。 如果你不但想对一个特定项的更改做出反应,而且对添加或删除该项也做出反应的话,那么 observable 映射会非常有用。
observable.map(values) 中的 values 可以是对象、 数组或者字符串键的 ES6 map。
下列 observable 映射所暴露的方法是依据 ES6 Map 规范:
has(key) - 返回映射是否有提供键对应的项。注意键的存在本身就是可观察的。
set(key, value) - 把给定键的值设置为 value 。提供的键如果在映射中不存在的话,那么它会被添加到映射之中。
delete(key) - 把给定键和它的值从映射中删除。
get(key) - 返回给定键的值(或 undefined)。
keys() - 返回映射中存在的所有键的迭代器。插入顺序会被保留。
values() - 返回映射中存在的所有值的迭代器。插入顺序会被保留。
entries() - 返回一个(保留插入顺序)的数组的迭代器,映射中的每个键值对都会对应数组中的一项 [key, value]。
forEach(callback:(value, key, map) => void, thisArg?) - 为映射中每个键值对调用给定的回调函数。
clear() - 移除映射中的所有项。
size - 返回映射中项的数量。
以下函数不属于 ES6 规范,而是由 MobX 提供:
toJS() - 将 observable 映射转换成普通映射。
toJSON(). 返回此映射的浅式普通对象表示。(想要深拷贝,请使用 mobx.toJS(map))。
intercept(interceptor) - 可以用来在任何变化作用于映射前将其拦截。参见 observe & intercept。
observe(listener, fireImmediately?) - 注册侦听器,在映射中的每个更改时触发,类似于为 Object.observe 发出的事件。想了解更多详情,请参见 observe & intercept。
merge(values) - 把提供对象的所有项拷贝到映射中。values 可以是普通对象、entries 数组或者 ES6 字符串键的映射。
replace(values) - 用提供值替换映射全部内容。是 .clear().merge(values) 的简写形式。

observable.map(values, { deep: false })

任何分配给 observable 映射的值都会默认通过 observable 来传递使其转变成可观察的。 创建浅映射以禁用此行为,并按原样存储值。关于此机制的更多详情,请参见 装饰器。

observable.map(values, { name: "my map" })

name 选项用来给数组一个友好的调试名称,用于 spy 或者 MobX 开发者工具。

observable原值类型值和引用类型值

JavaScript 中的所有原始类型值都是不可变的,因此它们都是不可观察的。 通常这是好的,因为 MobX 通常可以使包含值的属性转变成可观察的。
可参见 observable objects。 在极少数情况下,拥有一个不属于某个对象的可观察的“原始类型值”还是很方便的。 对于这种情况,可以创建一个 observable box 以便管理这样的原始类型值。

observable.box(value)

observable.box(value) 接收任何值并把值存储到箱子中。 使用 .get() 可以获取当前值,使用 .set(newValue) 可以更新值。

此外,还可以使用它的 .observe 方法注册回调,以监听对存储值的更改。 但因为 MobX 自动追踪了箱子的变化,在绝大多数情况下最好还是使用像 mobx.autorun 这样的 reaction 来替代。

observable.box(scalar) 返回的对象签名是:

.get() - 返回当前值。
.set(value) - 替换当前存储的值并通知所有观察者。
intercept(interceptor) - 可以用来在任何变化应用前将其拦截。参见 observe & intercept。
.observe(callback: (change) => void, fireImmediately = false): disposerFunction - 注册一个观察者函数,每次存储值被替换时触发。返回一个函数以取消观察者。参见 observe & intercept。change 参数是一个对象,其中包含 observable 的 newValue 和 oldValue 。
observable.box(value, { deep: false })
创建一个基于 ref 装饰器的箱子。这意味着箱子里的任何(将来)值都不会自动地转换成 observable 。

import {observable} from "mobx";

const cityName = observable.box("Vienna");

console.log(cityName.get());
// 输出 'Vienna'

cityName.observe(function(change) {
    console.log(change.oldValue, "->", change.newValue);
});

cityName.set("Amsterdam");
// 输出 'Vienna -> Amsterdam'

装饰器

MobX 有一组装饰器来定义 observable 属性的行为。

observable: observable.deep 的别名 observable.deep: 任何 observable
都使用的默认的调节器。它将任何(尚未成为 observable )数组,映射或纯对象克隆并转换为 observable
对象,并将其赋值给给定属性 observable.ref: 禁用自动的 observable 转换,只是创建一个 observable 引用
observable.shallow: 只能与集合组合使用。 将任何分配的集合转换为 observable,但该集合的值将按原样处理
observable.struct: 就像 ref, 但会忽略结构上等于当前值的新值 computed: 创建一个衍生属性, 参见
computed computed(options): 同 computed , 可设置选项 computed.struct: 与
computed 相同,但是只有当视图产生的值与之前的值结构上有不同时,才通知它的观察者 action: 创建一个动作, 参见 action
action(name): 创建一个动作,重载了名称 action.bound: 创建一个动作, 并将 this 绑定到了实例
装饰器可以使用 API decorate、observable.object、extendObservable 和 observable
(创建对象时) 来指定对象成员的行为。 如果没有传入装饰器,默认为对任意键值对使用 observable.deep,对 getters 使用
computed 。

import {observable, autorun, action} from "mobx";

var person = observable({
    name: "John",
    age: 42,
    showAge: false,

    get labelText() {
        return this.showAge ? `${this.name} (age: ${this.age})` : this.name;
    },

    // 动作:
    setAge(age) {
        this.age = age;
    }
}, {
    setAge: action
    // 其他属性默认为 observables / computed
});

class Person {
    name = "John"
    age = 42
    showAge = false

    get labelText() {
        return this.showAge ? `${this.name} (age: ${this.age})` : this.name;
    }

    setAge(age) {
        this.age = age;
    }
}
// 使用 decorate 时,所有字段都应该指定 (毕竟,类里的非 observable 字段可能会更多)
decorate(Person, {
    name: observable,
    age: observable,
    showAge: observable,
    labelText: computed,
    setAge: action
})

对observable作出反应

不要把 computed 和 autorun 搞混。它们都是响应式调用的表达式,但是,如果你想响应式的产生一个可以被其它 observer 使用的值,请使用 @computed,如果你不想产生一个新值,而想要达到一个效果,请使用 autorun。 举例来说,效果是像打印日志、发起网络请求等这样命令式的副作用。
如果你有一个函数应该自动运行,但不会产生一个新的值,请使用autorun。 其余情况都应该使用 computed。

@computed

import {observable, computed} from "mobx";

class OrderLine {
    @observable price = 0;
    @observable amount = 1;

    constructor(price) {
        this.price = price;
    }

    @computed get total() {
        return this.price * this.amount;
    }
}

**

Autorun

** 接收第二个参数,它是一个参数对象,有如下可选的参数:

delay: 可用于对效果函数进行去抖动的数字(以毫秒为单位)。如果是 0(默认值) 的话,那么不会进行去抖。 name:
字符串,用于在例如像 spy 这样事件中用作此 reaction 的名称。 onError: 用来处理 reaction
的错误,而不是传播它们。 scheduler: 设置自定义调度器以决定如何调度 autorun 函数的重新运行

autorun(() => {
    // 假设 profile.asJson 返回的是 observable Json 表示,
    // 每次变化时将其发送给服务器,但发送前至少要等300毫秒。
    // 当发送后,profile.asJson 的最新值会被使用。
    sendProfileToServer(profile.asJson);
}, { delay: 300 });

when(predicate: () => boolean, effect?: () => void, options?)

when 观察并运行给定的 predicate,直到返回true。 一旦返回 true,给定的 effect 就会被执行,然后 autorunner(自动运行程序) 会被清理。 该函数返回一个清理器以提前取消自动运行程序。

对于以响应式方式来进行处理或者取消,此函数非常有用。
Mobx的核心就是通过observable观察某一个变量,当该变量产生变化时,对应的autorun内的回调函数就会发生变化。

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了。
具体源码如下:

function autorun(view, opts){
    reaction = new Reaction(name, function () {
           this.track(reactionRunner);
    }, opts.onError);
   function reactionRunner() {
        view(reaction);
    }
}

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

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

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

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中会将回调与其绑定,之后更改了obserable中的值时,都会触发这里的set,然后随即触发绑定的函数。

Reaction

用法: reaction(() => data, (data, reaction) => { sideEffect }, options?)。

autorun 的变种,对于如何追踪 observable 赋予了更细粒度的控制。 它接收两个函数参数,第一个**(数据 函数)是用来追踪并返回数据作为第二个函数(效果 函数)的输入**。 不同于 autorun 的是当创建时效果 函数不会直接运行,只有在数据表达式首次返回一个新值后才会运行。 在执行 效果 函数时访问的任何 observable 都不会被追踪。

reaction 返回一个清理函数。

传入 reaction 的第二个函数(副作用函数)当调用时会接收两个参数。 第一个参数是由 data 函数返回的值。 第二个参数是当前的 reaction,可以用来在执行期间清理 reaction

值得注意的是 效果 函数仅对数据函数中访问的数据作出反应,这可能会比实际在效果函数使用的数据要少。 此外,效果 函数只会在表达式返回的数据发生更改时触发。 换句话说: reaction需要你生产 效果 函数中所需要的东西。

Reaction 接收第三个参数,它是一个参数对象,有如下可选的参数:

fireImmediately: 布尔值,用来标识效果函数是否在数据函数第一次运行后立即触发。默认值是 false 。 delay:
可用于对效果函数进行去抖动的数字(以毫秒为单位)。如果是 0(默认值) 的话,那么不会进行去抖。 equals: 默认值是
comparer.default 。如果指定的话,这个比较器函数被用来比较由 数据 函数产生的前一个值和后一个值。只有比较器函数返回
false 效果 函数才会被调用。此选项如果指定的话,会覆盖 compareStructural 选项。 name: 字符串,用于在例如像
spy 这样事件中用作此 reaction 的名称。 onError: 用来处理 reaction 的错误,而不是传播它们。
scheduler: 设置自定义调度器以决定如何调度 autorun 函数的重新运行

改变observable

action

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

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

Mobx.configure({enforceActions: true})

其原理也很简单

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。

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

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来简化操作。

 @action
  changeA() {
    this.a = 0;
    setTimeout(
      runInAction(() => {
        this.a = 1000;
      }),
      1000
    );
  }

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

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

@asyncAction
  changeA() {
    this.a = 0;
    const data = yield Promise.resolve(1)
    this.a = data;
  }

Mobx的一些坑

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

1. 无法收集新增的属性

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)方法来实现。

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。

2. 回调函数若依赖外部环境,则无法进行收集

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那一行代码,所以收集不到。

参考链接:

  1. https://www.zhihu.com/question/52219898
  2. http://taobaofed.org/blog/2016/08/18/react-redux-connect
  3. https://Mobx.js.org/index.html

你可能感兴趣的:(React,Mobx)