原文连接:github.com/whinc/blog/…javascript
导读:MobX 是一个优秀的响应式状态管理库,在流行的状态管理库 Redux 以外为咱们提供了其余选择。若是你尚未尝试过 MobX,我强烈建议你继续阅读本文,并跟着示例动手实践体验一下。本文是 MobX 的入门教程,文章包含 MobX 的设计哲学、状态派生模型、核心API以及与 React 的集成。html
MobX 是一个简单、可伸缩的响应式状态管理库。经过 MobX 你能够用最直观的方式修改状态,其余的一切 MobX 都会为你处理好(如自动更新UI),而且具备很是高的性能。java
MobX 文档的概念和 API 比较多,初次接触时可能会感受无从下手或者抓不住重点,本文尝试提供一份 MobX 核心的知识的简明教程,做为阅读官方文档以前的热身。react
本文不包含任何跟装饰器(decorator)相关的内容,由于装饰器并非 MobX 的核心内容,它只是语法糖,没有它照样可使用 MobX。git
目录github
在学习使用 MobX API 以前,咱们首先要了解 MobX 的设计哲学,它是咱们在思考 MobX 应用时的心智模型,能够帮助咱们更好的使用 MobX API。typescript
MobX 的设计哲学归纳起来就一句话:Anything that can be derived from the application state, should be derived. Automatically. (任何能够从应用状态中派生的内容,都应当自动地被派生。)api
理解这句话的关键是搞清楚【派生】的含义是什么?在 MobX 中【派生】的含义比较普遍,包括:数组
咱们平时常看到的状态响应模型,其中的响应就能够看作是状态的一种派生,MobX 将这种模型进行泛化,造成更通用的状态派生模型,接下来会详细介绍。缓存
状态响应模型归纳起来主要包含三个要素:定义状态、响应状态、修改状态(以下图所示)。
MobX 中经过observable
来定义可观察状态, 它接受任意 JS 值(包括Object
、Array
、Map
、Set
),返回原始数据的代理对象,代理对象与原始数据具备相同的接口,你能够把代理对象当作原始数据使用。
// 定义状态
const store = observable({
count: 0
});
复制代码
MobX 中经过autorun
来定义状态变化时要执行的响应操做,它接受一个函数。此后,每当observable
中定义的状态发生变化时,MobX 都会当即执行该函数。
// 响应状态
autorun(() => {
console.log("count:", store.count);
});
复制代码
MobX 中修改状态和修改原始数据的方式没什么区别,这也是 MobX 的优势——符合直觉的操做方式。
// 修改状态
store.count += 1;
复制代码
把上面的部分串起来就是一个最简单的 MobX 示例了(以下),示例中每次修改 count 的值时会自动打印一条日志,而且日志包含最新的 count 值。这个示例揭示了 MobX 中最核心的功能。
import { observable, autorun } from "mobx";
// 1. 定义状态
const store = observable({
count: 0
});
// 2. 响应状态
autorun(() => {
console.log("count:", store.count);
});
// count: 0
// 3. 修改状态
store.count += 1;
// count: 1
复制代码
上面例子中,首先经过 MobX 提供的observable
函数定义状态,例子的状态是{count: 0}
。而后经过经过 MobX 提供的autorun
函数定义状态响应函数,例子中是一条打印当前count
值得的语句,当定义的状态中的任何值发生变化时,该响应函数会当即执行,而且是由 MobX 自动完成的。最后是修改状态,和操做对象属性同样,例子中是对count
属性进行自增操做。
从上一节中咱们知道 MobX 的状态响应函数是在状态变化时执行某些操做。实际应用中这些操做可分为两类:带反作用(如打印日志、渲染 UI、请求网络等)和不带反作用(如计算数组的长度)。MobX 将这些操做统称为派生(Derivations),便可从应用状态中派生出来的任何内容。
其中不带反作用的操做是一个纯函数,通常是根据当前状态计算返回一个新的值。为了区分带反作用和不带反作用的两类状况,MobX 将 Derivations 概念细分红两个概念 Reactions 和 Computed values 来区分两者。此外,MobX 还提供了一个可选的概念 Action 来表示对 State 的修改操做,用于约束和预测应用状态的修改行为。这些概念汇总在一块儿以下图:
下面是一个简单的示例,完整展现了上图中涉及的全部概念。
index.html 文件
<html>
<body>
<div id="container"></div>
</body>
</html>
复制代码
index.js 文件
import { observable, autorun, computed, action } from "mobx"
const containerEl = document.querySelector("#container");
// State
const store = observable({
count: 0
});
// Actions
window.increaseCount = action(() => store.count++);
// Computed values
const doubleCount = computed(() => 2 * store.count);
// Reactions
autorun(() => {
containerEl.innerHTML = ` <span>${store.count} * 2 = ${doubleCount.get()}</span> <button onclick='window.increaseCount()'>+1</button> `;
});
// 0 * 2 = 0
// (点击按钮)
// 1 * 2 = 2
// (点击按钮)
// 2 * 2 = 4
复制代码
这个示例展现了一个简单的乘法,点击按钮后数据会自增,同时计算的结果也随之更新。相比上一个例子,它有几个值得注意的区别:
computed
定义计算值,并返回计算值对象doubleCount
,经过其get/set
方法访问内部计算值。computed
接受一个返回计算值的函数,在函数内部可使用observable
定义的状态数据(如count
),每当count
的值发生变化时,doubleCount
内部的计算值会自动更新。action
定义状态修改操做,action
接受一个函数,并返回一个签名相同的函数。在函数内部可直接修改obsevable
定义的状态(如count
)触发autorun
和computed
从新运行。autorun
中的响应操做替换成了渲染 UI,每当状态发生变化时从新渲染 UI。MobX 的 API 可分为四类:定义状态(observable
)、响应状态(autorun
, computed
)、修改状态(action
)、辅助函数。下面挑出最核心的 API 进行重点介绍,但不会涉及 API 的详细用法(请参考MobX Api Reference)。
MobX 目前同时支持 v4 和 v5 两个版本,这两个版本的 API 是相同的(功能相同),他们的区别在于内部实现数据响应的方式不一样,若是使用 v4 版本时须要注意文档中标注的注意状况。
observable
observable
用于定义可观察状态,其类型定义以下(已简化):
<T extends Object>(value: T): T & IObservableObject;
<T = any>(value: T[]): IObservableArray<T>;
<K = any, V = any>(value: Map<K, V>): ObservableMap<K, V>;
<T = any>(value: Set<T>): ObservableSet<T>;
复制代码
它接受Object/Array/Map/Set
类型的数据做为参数,并返回对应数据的代理对象,代理对象和原数据类型具备相同的接口,你能够像使用原始数据同样使用代理对象。例如:
import { observable } from "mobx";
const object = observable({a: 1})
console.log(object.a)
const array = observable([1, 2])
console.log(array[0])
const map = observable(new Map({a: 1}))
console.log(map.get('a'))
const set = observable(new Set([1, 2]))
console.log(set.has(1))
复制代码
observable
观察的数据能够嵌套,嵌套的数据也会被观察。例如:
import { observable, autorun } from "mobx";
const store = observable({
a: {
b: [1, 2]
}
})
autorun(() => console.log(store.a.b[0]))
// 1
store.a.b[0] += 1
// 2
复制代码
observable
支持动态添加可观察状态。例如:
import { observable, autorun } from "mobx";
const store = observable({});
autorun(() => {
console.log("a =", store.a);
});
// a = undefined
store.a = 1;
// a = 1
复制代码
动态添加可观察状态,仅适用于 MobX v5+ 版本,MobX v4 及如下版本须要借助辅助函数,详见Direct Observable manipulation。
autorun
autorun
用于定义响应函数,其类型定义以下(已简化):
autorun(reaction: () => any): IReactionDisposer;
复制代码
autorun
接受一个响应函数 reaction,并在定义时当即执行一次 reaction 函数, reaction 函数内部能够执行带有反作用的操做。 之后,每当依赖状态发生变化时,autorun
自动从新运行 reaction 函数。autorun
第一次运行 reaction 函数是为了搜集依赖状态——运行 reaction 过程当中实际使用的状态(经过obj.name
或obj['name']
解引用方式使用的状态)。
例以下面例子,autorun
中使用了状态a
,所以当状态a
的值发生变化时,会执行响应函数。而状态b
虽然被列为可观察状态,但因为在autorun
中没有被实际使用,所以当状态b
的值发生变化时,不会执行 响应函数 。这是 MobX 的“聪明”之处,它能根据状态的实际使用状况,细粒度地控制更新范围。因为减小了没必要要的执行开销,从而提高了程序性能。
import { observable, autorun } from "mobx";
const store = observable({
a: 1,
b: 2
});
autorun(() => {
console.log("a =", store.a);
});
// a = 1
store.a += 1;
// a = 2
store.b += 1;
// (无输出)
复制代码
让咱们回顾一下 MobX 的使用,经过observable
定义状态,在autorun
中使用状态,当autorun
中使用到的状态发生变化时,该autorun
从新执行。绝大部分状况下它都工做的很好,若是你遇到修改了状态而autorun
没有如你预期的那样运行,这时候你须要深刻了解 MobX 是如何响应状态变化的,推荐阅读What does MobX react to?。
computed
computed
用于定义计算值,其类型定义以下(已简化):
<T>(func: () => T) => { get(): T, set(value: T): void}
复制代码
computed
与autorun
类似,他们都会在依赖的状态发生变化时会从新运行,不一样之处是computed
接收的是纯函数而且返回一个计算值,这个计算值在状态变化时会自动更新,计算值能够在autorun
中使用。
例以下面例子中,ca
是一个计算值,它依赖状态a
的值,当状态a
的值发生变化时,ca
会从新计算值。计算值ca
是一个“装箱”对象,须要经过get/set
访问内部值,只有这样才能保持计算值的引用不变而内部值又是可变的。
import { observable, autorun, computed } from "mobx";
const store = observable({
a: 1
});
const ca = computed(() => {
return 10 * store.a;
});
autorun(() => {
console.log(`${store.a} * 10 = ${ca.get()}`);
});
// 1 * 10 = 10
store.a += 1;
// 2 * 10 = 20
store.a += 1;
// 3 * 10 = 30
复制代码
因为computed
被视做是纯函数,MobX 提供了许多开箱即用的优化措施,例如对计算值的缓存和惰性计算。
computed
值会被缓存
每当读取computed
值时,若是其依赖的状态或其余computed
值未发生变化,则使用上次的缓存结果,以减小计算开销,对于复杂的computed
值,缓存能够大大提升性能。
例以下面例子中,computed
值ca
和cb
分别依赖状态a
和b
,第一次执行autorun
时,ca
和cb
都会从新计算,而后修改状态a
的值,第二次执行autorun
时,只有ca
会从新计算,而cb
则使用上次的缓存结果。
import { observable, autorun, computed } from "mobx";
const store = observable({
a: 1,
b: 1
});
const ca = computed(() => {
console.log("recomputed ca");
return 10 * store.a;
});
const cb = computed(() => {
console.log("recomputed cb");
return 10 * store.b;
});
autorun(() => {
console.log(
`a = ${store.a}, ca = ${ca.get()}, b = ${store.b}, cb = ${cb.get()}`
);
});
// recomputed ca
// recomputed cb
// a = 1, ca = 10, b = 1, cb = 10
store.a += 1;
// recomputed ca
// a = 2, ca = 20, b = 1, cb = 10
复制代码
为了观察
computed
的计算过程,插入了打印日志的语句,这会带有反作用,实际中不要这样作
computed
值会惰性计算
只有computed
值被使用时才从新计算值。反言之,即便computed
值依赖的状态发生了变化,可是它暂时没有被使用,那么它不会从新计算。
例以下面例子中,computed
值ca
依赖状态a
。当状态a
的值小于 3 时,autorun
运行时只打印状态a
的值,因为computed
值ca
未被使用,因此ca
不会重新计算。当状态a
的值增加到 3 之后,autorun
运行时同时打印状态a
和computed
值ca
,因为computed
值ca
被使用了,因此ca
会从新计算。
import { observable, autorun, computed } from "mobx";
const store = observable({
a: 1
});
const ca = computed(() => {
console.log("recomputed ca");
return 10 * store.a;
});
autorun(() => {
if (store.a >= 3) {
console.log(`a = ${store.a}, ca = ${ca.get()}`);
} else {
console.log(`a = ${store.a}`);
}
});
// a = 1
store.a += 1;
// a = 2
store.a += 1;
// recomputed ca
// a = 3, ca = 30
复制代码
action
action
用于定义状态修改操做,其类型定义以下(已简化):
<T extends Function>(fn: T) => T
复制代码
虽然没有action
也能够直接修改状态,可是经过action
显式地修改状态,使得状态的变化可预测(状态的变化能定位到是哪一个action
引发的)。此外,action
函数是事务型的,经过action
修改状态时,响应函数不会当即执行,而是等到action
结束后才执行,这有助于提高性能。
例以下面例子中,展现了修改状态的两种方式,一种是直接修改状态a
,另外一种是经过调用预先定义的action
修改状态。值得注意的是,在一次action
中连续两次修改状态a
的值,只会触发一次autorun
的执行。
import { observable, autorun, action } from "mobx";
const store = observable({
a: 1
});
autorun(() => {
console.log(`a = ${store.a}`);
});
// a = 1
store.a += 1;
// a = 2
store.a += 1;
// a = 3
const increaseA = action(() => {
store.a += 1;
store.a += 1;
});
increaseA();
// a = 5
复制代码
对 MobX 进行一些配置后,可使action
成为修改状态的惟一方式,这能够避免不受约束的修改状态行为发生,有利于提高项目的可维护性。
例以下面例子中,配置 enforceActions 为"always"
后,就只能经过action
修改状态了,若是尝试直接修改状态将会触发异常。
import { observable, autorun, action, configure } from "mobx";
// 强制只能经过 action 修改状态
configure({
enforceActions: "always"
});
const store = observable({
a: 1
});
autorun(() => {
console.log(`a = ${store.a}`);
});
// a = 1
// store.a += 1;
// 直接修改状态,将会抛出以下异常
// Error: [mobx] Since strict-mode is enabled, changing observed
// observable values outside actions is not allowed.
// Please wrap the code in an `action` if this change is intended.
const increaseA = action(() => {
store.a += 1;
});
increaseA();
// a = 2
复制代码
MobX 是框架无关的,你能够单独使用它,也能够与任何流行的 UI 框架进行一块儿使用,MobX 官方提供了 React/Vue/Angular 等流行框架的绑定实现。MobX 最多见的是与 React 一块儿使用,他们的绑定实现是 mobx-react(或 mobx-react-lite)。
mobx-react 提供了一个observer
方法, 它是一个高阶组件,它接收 React 组件并返回一个新的 React 组件,返回的新组件能响应(经过observable
定义的)状态的变化,即组件能在可观察状态变化时自动更新。observer
方法是对 MobX 提供的autorun
方法和 React 组件更新机制的封装,以便于在 React 中使用,你依然能够在 React 中使用autorun
来更新组件。下面是observer
方法的类型声明,它支持组件类和函数组件。
function observer<T extends React.ComponentClass | React.FunctionComponent>(target: T): T 复制代码
下面是组件类使用 MobX 的示例,经过observable
定义可观察状态,并经过observer
包裹组件,以后组件事件处理方法中修改状态后,组件会自动更新,无需手动调用 React 的setState()
来更新组件。
import React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
class Counter extends React.Component {
constructor(props) {
super(props);
this.store = observable({
count: 0
});
}
render() {
return (
<button onClick={() => this.store.count++}> {this.store.count} </button>
)
}
}
export default observer(Counter);
复制代码
下面是函数组件使用 MobX 的示例,与上面类组件相似,区别是使用 mobx-react 提供的useLocalStore
定义客观察状态,useLocalStore
内部也是使用observable
定义可观察状态。
import React, { useMemo } from "react";
import { observable } from "mobx";
import { observer, useLocalStore } from "mobx-react";
const Counter = () => {
const store = useLocalStore(() => ({
count: 0
}));
// 等价于下面
// const store = useMemo(() => observable({ count: 0 }), []);
return (
<button onClick={() => store.count++}> {store.count} </button>
)
};
export default observer(Counter);
复制代码
MobX 的设计哲学是“可从应用状态中派生的任何内容都应当自动的被派生”,后半句有两个关键字:自动和派生。心中秉持这一设计哲学,再来看 MobX 的派生状态模型就比较清晰了,Computed values 和 Reactions 均可以视做是从 State 中派生出的,State 变化时触发 Computed values 的从新计算和 Reations 的从新运行。为了让派生能自动的进行,MobX 经过Object.definePropery
或Proxy
方式拦截对象的读写操做,从而容许用户以天然的方式来修改状态,MobX 负责更新派生的内容。
MobX 提供了几个核心 API 来帮助定义状态(observable
)、响应状态(autorun
, computed
)和修改状态(action
),经过这些 API 可让程序当即具有响应式能力。这些 API 接口并不复杂,但要熟练使用,须要深刻理解 MobX 响应机制,文中经过一些简单的示例来辅助理解这些 API 的行为。
MobX 能够单独使用,也能够与任何流行的 UI 框架一块儿使用,Github 上能够找到 MobX 与流行框架的绑定实现。不过 MobX 最多见的是与 React 一块儿使用,mobx-react 是流行的 MobX 和 React 的绑定实现库,本文介绍了它在组件类和函数组件上的一些基本用法。