投身烈火
Keep Walking

MobX入坑指南(3) -- Observable Types

这周阅读了一部分mobx的源码,和作者写的几篇介绍文章,发现这个库有些特性还挺有趣的。以下的内容大部分翻译自mobx文档Observable Types 一节,奈何小生英文水平有限,没有划词软件帮忙就看不懂句子……所以翻的很渣,基本上都是掺杂了一些我的理解在里边,连蒙带猜拼出来的,有啥写的不对的,也请多指教了~( ̄▽ ̄)~*

在mobx中,如果你想监听某个变量的变化,需要先使用 observable 函数将其转化为生成Observable对象才行。这个章节主要讲的是 observable 函数生成的不同Observable对象

Observable Objects

observable 函数接受的参数是个普通的javascript object(普通javascript对象是指不是通过构造函数生成的对象,mobx的判断方式是,通过getPrototypeOf获取原型,检查是不是Object.prototype或者null)时,这个对象中的所有属性都将被传入 observable 函数进行转换,如果属性值是object或者array,则会递归的转化内部的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import {observable, autorun, action} from "mobx";
var person = observable({
// 常量会自动使用observable转化:
name: "John",
age: 42,
showAge: false,
// 有get描述符的属性会自动使用computed转化:
get labelText() {
return this.showAge ? `${this.name} (age: ${this.age})` : this.name;
},
// 如果直接写
// labelText: function () {
// return this.showAge ? `${this.name} (age: ${this.age})` : this.name;
// }
// 在自动转化时会报一个warning,mobx认为渲染函数需要用computed显式调动一下
// labelText: computed(function () {
// return this.showAge ? `${this.name} (age: ${this.age})` : this.name;
// })
// 同理,action也需要显式调动一下:
setAge: action(function(age) {
this.age = age;
})
});
autorun(() => console.log(person.labelText));
person.name = "Dave";
// 'Dave'
person.setAge(21);
console.log(person.age);
// 21

需要注意的点:

  • 当对象传入 observable 函数时,只有当时对象上已经存在的属性才能被监听,后添加的属性是不能的,如果想要往已经生成的Observable对象上添加属性,需要使用extendObservable函数进行类似merge的操作。

    1
    2
    3
    4
    5
    6
    //接上面的例子
    extendObservable(person, {
    sex: 'male'
    });
    console.log(person.sex);
    // 'male'
  • 只有普通对象能够被转化为Observable对象。使用构造函数创建的对象,需要在构造函数初始化时使用extendObservable函数合并属性。使用类创建的对象,需要使用 @observable装饰器包装类的属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    var Person = function(firstName, lastName) {
    // 在一个新的对象里写要监听的属性,然后merge和当前实例merge
    extendObservable(this, {
    firstName: firstName,
    lastName: lastName
    });
    }
    var zxc = new Person("Zheng", "Xingcheng");
    // 或者
    class Person {
    @observable firstName:string = "";
    @observable lastName:string = "";
    constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    }
    }
    var zxc = new Person("Zheng", "Xingcheng");
  • 带有get描述符的属性会自动被@computed转化,第一个例子中已经展示了。

  • 如果对象的属性值是一个构造函数生成的对象(我真是不想把它叫成”非普通对象”……),那这个属性也不会被 observable 函数自动转化。
  • 基本上使用observable 函数自动转化已经能解决绝大部分的使用场景了(原文写的是”95% of the cases”……)。如果想要了解更详细的设定每个属性的方法,请看 modifiers 一章(本来这一章的内容也想发到这里的,但是实在是没时间写了只能拉倒……等下次吧)。

Observable Arrays

和 object 类似,array 也可以使用 observable函数转化为ObservableArray对象。当然,也是递归转化每个元素的,所以数组的所有元素也会被转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {observable, autorun} from "mobx";
var todos = observable([
{ title: "吃午饭", completed: true },
{ title: "喝咖啡", completed: false }
]);
autorun(() => {
console.log("代办:", todos
.filter(todo => !todo.completed)
.map(todo => todo.title)
.join(", ")
);
});
// '代办: 喝咖啡'
todos[0].completed = false;
// '代办: 吃午饭, 喝咖啡'
todos[2] = { title: "睡午觉", completed: false };
// '代办: 吃午饭, 喝咖啡, 睡午觉'
todos.shift();
// '代办: 喝咖啡, 睡午觉'

由于ES5中原生数组的限制(array.observe到ES7中才有,而且数组不能扩展),observable函数基于原始数组克隆了一个新的数组,这个新数组支持所有原生数组的方法和功能,同时还有监听值变化的能力。

不过,当使用Array.isArray检查包装后数组时返回的是false。所以在跟其他库联合使用时,如果想把ObservableArray对象当做数组传递给其他库,最好使用浅拷贝生成新数组,或者使用 array.slice 方法生成新数组,就是说 Array.isArray(observable([]).slice()) 返回的是true。

由于不喜欢原生的 array.sortarray.reverse 方法,ObservableArray对象的 sortreverse 方法是并不会改变自身的,而是返回一个新的ObservableArray对象。

除了原生数组支持的方法,ObservableArray对象还可以使用以下的方法:

  • intercept(interceptor)
    这个方法可以在所有数组的操作被应用之前,将操作拦截。具体的请看observe & intercept
  • observe(listener, fireImmediately? = false)
    这个方法可以监听数组的变化,回调函数会接收数组新增或者修改的元素,符合ES7的规范。这个方法返回一个注销函数用来停止监听。
  • clear()
    清空数组。
  • replace(newItems)
    替换数组里的所有元素。
  • find(predicate: (item, index, array) => boolean, thisArg?, fromIndex?)
    使用方法与ES7的 array.find 一致,但是增加了formIndex参数。
  • remove(value)
    移除数组中第一个值等于value的元素,如果移除成功会返回true。
  • peek()
    slice 类似,会返回一个包含所有元素的数组。它与 slice 的区别在于 peek 不会进行保护性拷贝,所以性能更好。

Observable Maps

observable(asMap(values?, modifier?))map(values?, modifier?)) 方法可以创建一个ObservableMap对象。如果你不想响应特定属性的变化,还要添加删除属性,那么使用ObservableMap对象很合适。不同于ES6的Map对象,ObservableMap对象是能用字符串当做key。

根据ES6 Map的规范,可以使用以下方法:

  • has(key)
    返回map中是否存在这个key,这个方法是可被监听的
  • set(key, value)
    设置key对应的value,如果key之前不错在,那么这个key会被添加上
  • delete(key)
    删除key和key对应的value
  • get(key)
    获取key对应的value,如果没找到会返回undefined
  • keys()
    获取map的所有key,顺序为key的插入顺序
  • values()
    获取map的所有key对应的value,顺序为key的插入顺序
  • entries()
    返回一个数组,数组中每个元素为一个数组,数组中的元素为map中的key/value对,形式如[key, value],顺序为key的插入顺序
  • forEach(callback:(value, key, map) => void, thisArg?)
    对map中的每个key/value对调用回调
  • clear()
    清除map中的所有key/value对
  • size()
    返回map中所有key/value对的数量

ObservableMap对象还提供了以下方法可以使用:

  • toJS()
    返回一个map的浅拷贝的对象,如果想获得深拷贝的对象,需要使用 mobx.toJS(map)
  • intercept(interceptor)
    注册一个拦截器,拦截器会在map被修改之前被触发。具体的请看observe & intercept
  • observe(listener, fireImmediately?)
    注册一个监听,map被修改时会被触发。与Object.observe类似。具体的请看observe & intercept
  • merge(object | map)
    将对象上所有的属性拷贝到当前map中

Primitive (常量)

在javascript中,常量是不可变的,所以也没办法观测。所以如果想要监听一个常量属性的变化,需要使用 observable 函数包装一下。

observable 函数包装后会返回一个ObservablePrimitive对象。这个对象使用get和set方法来获取和改变变量的值。可以使用 .observe 方法来监听值的变化。不过通常使用 mobx.autorun 是更好的选择。

以下是ObservablePrimitive支持的方法:

  • get()
    返回当前值
  • set(value)
    替换当前存储的值,并通知所有的监听器
  • intercept(interceptor)
    注册拦截器,当值发生变化前触发。具体的请看observe & intercept
  • observe(callback: (newValue, previousValue) => void, fireImmediately = false)
    注册监听器,当值发生变化时触发,返回一个注销函数。具体的请看observe & intercept
1
2
3
4
5
6
7
8
9
10
11
12
13
import {observable} from "mobx";
const cityName = observable("Vienna");
console.log(cityName.get());
// 'Vienna'
cityName.observe(function(newCity, oldCity) {
console.log(oldCity, "->", newCity);
});
cityName.set("Amsterdam");
// 'Vienna -> Amsterdam'

References (引用类型)

之前在ObservableObject中提到,observable在转换时会自动用computed包装函数,这个自动转换其实是由限制条件的。mobx只会转换渲染用的函数,既函数不能接受参数,如果接受参数,则认为这个是一个复合函数,就不会自动转化。同样的,如果对象中有属性是通过构造函数或者类创建的对象,也不会自动转化。他们都继续保持着转化之前的引用。

有时我们也会有这一类的需求,要求对象中某个属性不被自动转化。可以使用 asReference 来达到这一目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var test = observable({
x : 3,
doubler: function() {
return this.x*2;
},
someFunc: asReference(function() {
return this.x;
})
});
console.log(test.doubler);
// 6
console.log(test.someFunc);
// function() {
// return this.x;
// }

总结

总体感觉上,mobx的api和knockout的api很像,但是在实现原理上,更像rx。mobx实现监听变化调用回调的过程是同步的,通过这种方式就能会自动的分析各个变量间的调用关系,从而减少重复订阅的情况。同时计算最新的值时还是用了memoizing技术,这样就算调用频繁,性能上也能得到保证。

相关文章