主要组成
本节将详细介绍 MobX 的主要组成部分以及每一部分涉及的重要 API。
state
state 是驱动应用的数据,是应用的核心。同 Redux 类似,我们依然可以把 state 分为三类:与领域直接相关的领域状态数据、反映应用行为(登录状态、当前是否有 API 请求等)的应用状态数据和代表 UI 状态的 UI 状态数据。在实际使用中,一般还会另外创建一个 store 来管理 state,这和 Redux 中的 store 也是类似的。但 MobX 中,可以在一个应用中使用多个 store,store 中的 state 也是可变的。另外,MobX 的 state 的结构不需要做标准化处理(Normalize),可以有多层嵌套结构,以方便 UI 组件使用为指导原则,这也是和 Redux 的 state 不同的地方。
MobX 提供了 observable 和 @observable 两个 API 创建可观测的 state,用法如下:
observable(value)
@observable classProperty = value
这两个 API 几乎可以用在所有的 JS 数据类型上。但根据不同类型的值创建出的可观测 state 的表现行为是有不同点的:
普通对象(Plain Object)
普通对象指原型不存在或原型是 Object.prototype 的对象,例如,var obj={"book":"react"}、var obj = new Object({"book":"react"}) 都是普通对象。MobX 根据普通对象创建一个可观测的新对象,新对象的属性和普通对象相同,但每一个属性都是可观测的,例如:
import { observable, autorun } from "mobx";
var person = observable({
name: "Jack",
age: 20
});
// mobx.autorun 会创建一个 reaction 自动响应 state 的变化,后面将会介绍
autorun(() => console.log(`name:${person.name}, age: ${person.age}`));
person.name = "Tom";
// 输出:name:Tom, age:20
person.age = 25;
// 输出:name:Tom, age:25
person 的 name 和 age 属性都是可观测的,任意属性的变化都会触发 autorun 的重新执行。使用 @observable 可以将代码改写如下:
import { observable, autorun } from "mobx";
class Person {
@observable name = "Jack";
@observable age = 20;
}
var person = new Person();
// mobx.autorun 会创建一个 reaction 自动响应 state 的变化,后面将会介绍
autorun(() => console.log(`name:${person.name}, age: ${person.age}`));
person.name = "Tom";
// 输出:name:Tom, age:20
person.age = 25;
// 输出:name:Tom, age:25
使用普通对象转换成可观测对象时,还需要注意下面几个问题:
只有当前普通对象已经存在的属性才会转换成可观测的,后面添加的新属性都不会自动变成可观测的,例如:
import { observable, autorun } from "mobx";
var person = observable({
name: "Jack",
age: 20
});
autorun(() => console.log(`name:${person.name}, age: ${person.age}, address:${person.address}`));
person.address = "Shanghai";
// 没有新的输出
address 是后来添加的属性,它的改变并不会引起 autorun 的重新执行。属性的 getter 会自动转换成 computed value,效果和使用 @computed 相同。
import { observable, autorun } from "mobx";
var person = observable({
name: "Jack",
age: 20,
// 自动转换成 computed value
get labelText() {
return `name:${this.name},age:${this.age}`;
}
});
autorun(() => console.log(person.labelText));
person.name = "Tom";
// 输出:name:Tom, age:20
person.age = 25;
// 输出:name:Tom, age:25
labelText 是一个 getter 方法,会自动转换成 computed value,autorun 中使用到了 labelText,labelText 的计算值又依赖于 name 和 age,所以 name 和 age 的改变会导致 autorun 重新执行。
observable 会递归地遍历整个对象,每当遇到对象的属性值还是一个对象时(不包含非普通对象),这个属性值将会继续被 observable 转换,例如:
import { observable, autorun } from "mobx";
var person = observable({
name: "Jack",
address: {
province: "Shanghai",
district: "Pudong"
}
});
autorun(() =>
console.log(`name:${person.name},address:${JSON.stringify(person.address)}`)
);
person.address.district = "Xuhui";
// 输出:name:Jack, address:{"province":"Shanghai","district":"Xuhui"}
person 的 name 和 address 属性是可观测的,address 的值是一个对象,因此会继续被 observable 处理,address 的 province 和 district 属性也被转换成可观测的。所以,当 person.address.district ="Xuhui" 执行后,district 的改变也会导致 autorun 的重新执行。
此外,如果以后再给可观测属性赋新值并且新值是一个对象(不包含非普通对象)时,新值也会自动被转换成可观测的,例如:
import { observable, autorun } from "mobx";
var person = observable({
name: "Jack",
address: {
province: "Shanghai",
district: "Pudong"
}
});
autorun(() =>
console.log(`name:${person.name},address:${JSON.stringify(person.address)}`)
);
// 给可观测属性 address 赋新值
person.address = {
province: "Beijing",
district: "Xicheng"
};
// 输出:name:Jack,address:{"province":"Beijing","district":"Xicheng"}
person.address.district = "Dongcheng";
// 输出:name:Jack,address:{"province":"Beijing","district":"Dongcheng"}
person 的 address 被赋予一个新对象时,新对象被自动转换成可观测对象,因此,新对象的 district 属性发生改变后,autorun 依然会被触发。
ES 6 Map
返回一个新的可观测的 Map 对象。Map 对象的每一个对象都是可观测的,而且向 Map 对象中添加或删除新元素的行为也是可观测的,这也是 Map 类型的可观测 state 最大的特点,例如:
import { observable, autorun } from "mobx"
// Map可以接收一个数组作为参数,数组的每一个元素代表 Map 对象中的一个键值对
var map = new Map([["name", "Jack"], ["age", 20]]);
var person = observable(map);
autorun(() =>
console.log(`name:${person.get("name")}, age:${person.get("age")},address:${person.get("address)}`)
);
person.set("address", "Shanghai");
// 输出:name:Jack, age:20, address:Shanghai
person 是一个可观测的 Map 对象,当通过 Map 的 API 向 person 中添加新元素 address 时,autorun 会重新执行。
数组
返回一个新的可观测数组。数组元素的增加或减少都会自动被观测,例如:
import { observable, autorun } from "mobx";
var todos = observable(["Learn React", "Learn Redux"]);
autorun(() => console.log(`待办事项数量:${todos.length}`));
todos.push("Learn MobX");
// 输出:待办事项数量:3
todos.shift();
// 输出:待办事项数量:2
observable 作用于数组类型时,也会递归地作用于数组中的每个元素对象,处理规则和处理普通对象时的规则相同,例如:
import { observable, autorun } from "mobx";
var todos = observable([
{ text: "Learn React", finished: false },
{ text: "Learn Redux", finished: false }
]);
autorun(() => console.log(`todo 1: ${todos[0].text}, finished: ${todos[0].finished}`));
todos[0].finished = true;
// 输出:todo 1:Learn React, finished: true
todos 数组中的元素也转换成可观测对象,因此,元素属性的变化会导致 autorun 的重新执行。
非普通对象
这里,非普通对象的概念是针对普通对象而言的,特指以自定义函数作为构造函数创建的对象。observable 会返回一个特殊的 boxed values 类型的可观测对象。注意,返回的 boxed values 对象并不会把非普通对象的属性转换成可观测的,而是保存一个指向原对象的引用,这个引用是可观测的。对原对象的访问和修改需要通过新对象的 get() 和 set() 方法操作,例如:
import { observable, autorun } from "mobx";
function Person(name, age) {
this.name = name;
this.age = age;
}
var person = observable(new Person("Jack", 20));
// person 是 boxed values 类型,必须通过 get() 才能获取到原对象
autorun(() => console.log(`name:${person.get().name},age:${person.get().age}`));
person.get().age = 25
// 没有输出,因为 person 对象的属性不可观测
// person 封装的对象设置为一个新对象,引用发生变化,可观测
person.set(new Person("Jack", 20));
// 输出:name:Jack, age:20
将非普通对象的属性转换成可观测的是自定义构造函数的责任。正确的实现方式是:
import { extendObservable, autorun } from "mobx";
function Person(name, age) {
// 使用 extendObservable 在构造函数内创建可观测属性
extendObservable(this, {
name: name,
age: age
});
}
var person = new Person("Jack", 20);
autorun(() => console.log(`name:${person.name}, age:${person.age}`));
person.age = 25;
// 输出:name:Jack, age:25
改写成使用装饰器 @observable 的方式:
import { observable, autorun } from "mobx";
class Person {
@observable name;
@observable age;
constructor(name, age) {
this.name = name;
this.age = age
}
var person = new Person("Jack", 20);
autorun(() => console.log(`name:${person.name}, age:${person.age}`));
person.age = 25;
// 输出:name:Jack,age:25
}
基本数据类型
MobX 是将包含值的属性(引用)转换成可观测的,而不是直接把值转换成可观测的。当 observable 接收的参数是 JavaScript 的基本数据类型时,MobX 不会把它们转换成可观测的,而是同处理非普通对象一样,返回一个 boxed values 类型的对象,例如:
import { observable, autorun } from "mobx";
// Jack这个值是不可观测的,可观测的是指向这个对象的引用
const name = observable("Jack");
autorun(() => console.log(`name:${name.get()}`));
name.set("Tom");
// 输出:name:Tom
除了直接使用 observable 创建可观测对象外,还可以使用语义更加精确的 API 创建不同类型的可观测对象,例如:
observable.object(value) // 创建一个可观测的 Object
observable.array(value) // 创建一个可观测的 Array
observable.map(value) // 创建一个可观测的 Map
observable.box(value) // 创建一个可观测的 Boxed value
observable(value) 相当于以上 API 的简写方式,会自动根据参数类型的不同使用不同的转换逻辑。
computed value
computed value 是根据 state 衍生出的新值,新值必须是通过纯函数计算得到的。computed value 依赖的 state 改变时,会自动重新计算,前提是这个 computed value 有被 reaction 使用。也就是说,computed value 采用延迟更新策略,只有被使用时才会自动更新。一般通过 computed 和 @computed 创建 computed value,使用方式如下:
computed(() => expression)
@computed get classProperty() { return expression; }
computed 一般用于接收一个函数,例如:
import { observable, computed, autorun } from "mobx"
var person = observable.object({
name: "Jack",
age: 20,
});
// 使用 computed 函数创建 computed value
const isYoung = computed(() => {
return person.age < 25;
})
autorun(() =>
console.log(`name:${person.name},isYong:${isYong}`)
);
person.age = 25;
// 输出:name:Jack, isYoung:false
@computed 一般用于修饰 class 的属性的 getter 方法,例如:
import { observable, computed, autorun } from "mobx";
class Person {
@observable name;
@observable age;
// 使用 @computed 装饰器创建 computed value
@computed get isYoung() {
return this.age < 25;
}
constructor(name, age) {
this.name = name;
this.age = age
}
}
var person = new Person("Jack", 20);
autorun(() => console.log(`name:${person.name},isYong:${person.isYoung}`));
person.age = 25;
// 输出:name:Jack, isYong:false
reaction
reaction 是自动响应 state 变化的有副作用的函数。和 computed value 相同的地方是,它们都会因为 state 的变化而自动触发,所以 computed value 和 reaction 在 MobX 中都被称为 derivation(衍生)。derivation 是指可以从 state 中衍生出来的任何东西,例如值或者动作。与 computed value 不同的是,reaction 产生的不是一个值,而是执行一些有副作用的动作,例如打印信息到控制台、发送网络请求、根据 React 组件树更新 DOM 等。
使用 observer/@observer 封装 React 组件是常用的创建 reaction 的方式。observer/@observer 是 mobx-react 这个包提供的 API,常用的使用方式有如下三种:
observer((props, context) => ReactElement)
observer(class MyComponent extends React.Component {...})
@observer class MyComponent extends React.Component {...}
observer 的参数可以是一个 React 函数组件,也可以是一个 React 类组件,但对于类组件一般习惯使用 @observer 创建 reaction。observer/@observer 本质上是将组件的 render 方法转换成 reaction,当 render 依赖的 state 发送变化时,render 方法会被重新调用。
除了 observer/@observer 外,常用的创建 reaction 的 API 还有 autorun、reaction、when,这几个 API 直接作用于函数而不是组件。
autorun
用法:
autorun(() => { sideEffect })
autorun 在前面的例子中已经多次使用到。使用 autorun 时,它接收的函数会被立即触发执行一次,以后的执行就依赖于函数使用的 state 的变化了。autorun 会返回一个清除函数 disposer,当不再需要观察相关 state 的变化时,可以调用 disposer 函数清除副作用,例如:
var numbers = observable([1,2,3]);
var sum = computed(() => numbers.reduce((a,b) => a + b, 0));
var disposer = autorun(() => console.log(sum.get()));
// 输出:6
numbers.push(4);
// 输出:10
disposer(); // 清除 autorun
numbers.push(5);
// 没有输出
reaction
用法:
reaction(() => data, data => { sideEffect }, options?)
它接收两个函数,第一个函数返回被观测的 state,这个返回值同时是第二个函数的输入值,只有第一个函数的返回值发生变化时,第二个函数才会被执行。第三个参数 options 是可选参数,提供一些可选设置,一般很少用到。reaction 也会返回一个清除函数 disposer。可见,相较于 autorun,reaction 可以对跟踪哪些对象有更多的控制。下面是一个示例:
const todos = observable([
{
title: "Learn React",
done: true
},
{
title: "Learn MobX",
done: false
}
]);
// 错误用法:只响应 todos 数组长度的变化,不会响应 title 属性的变化
const reaction1 = reaction(
() => todos.length,
length => console.log("reaction 1:", todos.map(todo => todo.title).join(','))
);
// 正确用法:同时响应 todos 数组长度和 title 属性的变化
const reaction2 = reaction(
() => todos.map(todo => todo.title),
titles => console.log("reaction 2:", titles.join(", "))
);
todos.push({title: "Learn Redux", done: false });
// 输出:
// reaction 2: Learn React, Learn Mobx, Learn Redux
// reaction 1: Learn React, Learn Mobx, Learn Redux
todos[0].title = "Learn Something";
// 输出:
// reaction 2: Learn Something, Learn Mobx, Learn Redux
when
用户:
when(() => condition, () => { sideEffect })
condition 会自动响应它使用的任何 state 的变化,当 condition 返回 true 时,函数 sideEffect 会执行,且只执行一次。when 也会返回一个清除函数 disposer。when 非常适合用在以响应式的方式执行取消或清除逻辑的场景,例如:
class MyResource {
constructor() {
when(
() => !this.isVisible,
() => this.dispose()
);
}
@computed get isVisible() {
// 判断某个元素是否可见
}
dispose() {
// 清除逻辑
}
}
action
action 是用来修改 state 的函数。MobX 提供了 API action 和 @action 用来包装 action 函数,但这并不是必需的。当 MobX 运行在严格模式下(调用 mobx.useStrict(true) 即可启动严格模式)时,必须使用这两个 API 包装 action 函数。常见的用法有:
action(fn)
@action classMethod
为了让代码更加清晰可读,建议创建 action 函数时都要使用 action/@action。此外,action/@action 还能带来性能的提升,当函数内多次修改 state 时,action/@action 会执行批处理操作,只有所有的修改都执行完成后,才会通知相关的 computed value 和 reaction。下面是一个获取 BBS 帖子列表的 action:
@action fetchPostList(url) {
this.pendingRequestCount++;
return fetch(url).then(
action(data => {
this.pendingRequestCount--;
this.posts.push(data);
})
);
}
这里需要注意,我们使用了两次 action 函数,因为 fetch 是异步执行的,执行完成的回调函数中也会修改 state,所以需要单独使用一个 action 包装回调函数。
使用 action 时,需要注意函数内 this 指向的问题,例如:
class Ticker {
@observable tick = 0
@action
increment() {
this.tick++;
}
}
const ticker = new Ticker()
setInterval(ticker.increment, 1000) // 报错
在上面的例子中,increment 执行时,this 指向的是全局的 window 对象,并不是期望的 Ticker 的实例对象。可以使用箭头函数解决 this 绑定的问题:
class Ticker {
@observable tick = 0;
@action
increment = () => {
this.tick++;
}
}
const ticker = new Ticker();
setInterval(ticker.increment, 1000);
此外,MobX 还提供了 @action.bound 和 action.bound 两个 API 帮助完成 this 绑定的工作:
class Ticker {
@observable tick = 0;
@action.bound
increment() {
this.tick++;
}
}
const ticker = new Ticker();
setInterval(ticker.increment, 1000);
或
const ticker = observable({
tick: 1,
increment: action.bound(function () {
this.tick++;
})
})
setInterval(ticker.increment, 1000);