Mobx是通过函数响应式编程使状态管理变得简单和可扩展的状态管理库。
可以用任何你喜欢的数据结构来存储状态,如对象、数组、类。 循环数据结构、引用,都没有关系。 只要确保所有会随时间流逝而改变的属性打上 mobx 的标记使它们变得可观察即可。
import {observable} from 'mobx';
var appState = observable({
timer: 0
});
当 appState 中相关数据发生改变时视图会自动更新。 MobX 会以一种最小限度的方式来更新视图。
通常来说,任何函数都可以成为可以观察自身数据的响应式视图,MobX 可以在任何符合ES5的JavaScript环境中应用。 但是在这所用的示例是 ES6版本的 React 组件视图。
import {observer} from 'mobx-react';
@observer
class TimerView extends React.Component {
render() {
return (
<button onClick={this.onReset.bind(this)}>
Seconds passed: {this.props.appState.timer}
</button>
);
}
onReset() {
this.props.appState.resetTimer();
}
};
ReactDOM.render(<TimerView appState={appState} />, document.body);
下面的代码每秒都会修改你的数据,而当需要的时候UI会自动更新。 无论是在改变状态的控制器函数中,还是在应该更新的视图中,都没有明确的关系定义。 使用 observable 来装饰你的状态和视图,这足以让 MobX检测所有关系了。
appState.resetTimer = action(function reset() {
appState.timer = 0;
});
setInterval(action(function tick() {
appState.timer += 1;
}), 1000);
只有在严格模式(默认是不启用)下使用 MobX 时才需要 action 包装。 建议使用 action,因为它将帮助你更好地组织应用,并表达出一个函数修改状态的意图。
MobX 区分了以下几个应用中的概念。 在之前的要点中已经见过了,现在让我们更深入地了解它们。
状态 是驱动应用的数据。 通常有像待办事项列表这样的领域特定状态,还有像当前已选元素的视图状态。 记住,状态就像是有数据的excel表格。
任何 源自状态并且不会再有任何进一步的相互作用的东西就是衍生。 衍生以多种形式存在:
MobX 区分了两种类型的衍生:
黄金法则: 如果你想创建一个基于当前状态的值时,请使用 computed。
动作 是任一一段可以改变状态的代码。用户事件、后端数据推送、预定事件、等等。 动作类似于用户在excel单元格中输入一个新的值。
在 MobX 中可以显式地定义动作,它可以帮你把代码组织的更清晰。 如果是在严格模式下使用 MobX的话,MobX 会强制只有在动作之中才可以修改状态。
MobX 支持单向数据流,也就是动作改变状态,而状态的改变会更新所有受影响的视图。

当状态改变时,所有衍生都会进行原子级的自动更新。因此永远不可能观察到中间值。
所有衍生默认都是同步更新。这意味着例如动作可以在改变状态之后直接可以安全地检查计算值。
计算值 是延迟更新的。任何不在使用状态的计算值将不会更新,直到需要它进行副作用(I / O)操作时。 如果视图不再使用,那么它会自动被垃圾回收。
所有的计算值都应该是纯净的。它们不应该用来改变状态。
下面的代码清单举例说明了以上的概念和原则:
import {observable, autorun} from 'mobx';
var todoStore = observable({
/* 一些观察的状态 */
todos: [],
/* 推导值 */
get completedCount() {
return this.todos.filter(todo => todo.completed).length;
}
});
/* 观察状态改变的函数 */
autorun(function() {
console.log("Completed %d of %d items",
todoStore.completedCount,
todoStore.todos.length
);
});
/* ..以及一些改变状态的动作 */
todoStore.todos[0] = {
title: "Take a walk",
completed: false
};
// -> 同步打印 'Completed 0 of 1 items'
todoStore.todos[0].completed = true;
// -> 同步打印 'Completed 1 of 1 items'
Copy
修饰器是一个对类进行处理的函数。
基本语法
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
返回函数为修饰器传参
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false
添加实例属性
function testable(target) {
target.prototype.isTestable = true;
}
@testable
class MyTestableClass {}
let obj = new MyTestableClass();
obj.isTestable // true
实例:mixins
// mixins.js
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list)
}
}
// main.js
import { mixins } from './mixins'
const Foo = {
foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() // 'foo'
属性的修饰
属性修饰器的参数:
第一个参数是类的原型对象,上例是Person.prototype修饰器的本意是要“修饰”类的实例,但是这个时候实例还没生成,所以只能去修饰原型(这不同于类的修饰,那种情况时target参数指的是类本身)
第二个参数是所要修饰的属性名
第三个参数是该属性的描述对象
更多:https://segmentfault.com/a/1190000013051904
什么是 Observable?
Observable 是一种让数据的变化可以被观察的方法。
哪些数据可被观察?
原始类型
对象
数组
。。。
标记observable的三种方式总结
1: 使用@observable
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
}
}
2: 使用observable()
import { observable } from "mobx"
let person = observable({name: 'emma', weight: 50})
3: 使用decorate()
import { decorate, observable, computed } from "mobx"
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 时,所有字段都应该指定
decorate(Person, {
name: observable,
age: observable,
showAge: observable,
labelText: computed,
setAge: action
})
computed
autorun
when
reaction
如果使用了@computed get getValue,那么getValue将会被缓存,如果value没有改变,那么getValue也不会改变,其它组件也不会收到通知。
此外如果读取getValue的值,通常会得到一个缓存的值,而不带@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;
}
}
引入computed的4种方式
1:使用@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;
}
}
2: 使用computed
import {observable, computed} from 'mobx'
let numbers = observable([1, 2, 3])
let sum = computed(()=>numbers.reduce((a, b)=>a + b, 0))
3: 使用decorate
import { observable, computed, decorate } from "mobx"
class OrderLine {
price = 0;
amount = 1;
constructor(price){
this.price = price;
}
get total(){
return this.price * this.amount;
}
}
decorate(OrderLine, {
price: observable,
amount: observable,
total: computed
});
4:默认是computed的情况
observable.object和extendObservable会自动把getter属性标记为computed:
import { observable, computed, decorate } from "mobx"
const OrderLine = observable.object({
price: 0,
amount: 1,
get total(){
return this.price * this.amount;
}
})
当使用 autorun 时,所提供的函数总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发。 相比之
下,computed(function) 创建的函数只有当它有自己的观察者时才会重新计算,否则它的值会被认为是不相关的。
经验法则:如果你有一个函数应该自动运行,但不会产生一个新的值,请使用autorun。 其余情况都应该使用 computed。
class store{
@observable count = 0;
@observable foo ='bar'
}
const store = new Store();
autorun(() => {
console.log('autorun:',store.count)
})
store.count = 10
store.foo = 'hello'
//执行结果如下:
autorun: 0
autorun:10
observable可以用来观测一个数据,这个数据可以数字、字符串、数组、对象等类型,而当观测到的数据发生变化的时候,如果变化的值处在autorun中,那么autorun就会自动执行。
上例中的autorun函数中,只对count值进行了操作,所以store.foo = 'hello’这步并不会触发autorun,只有count的变化才触发了autorun。
class store{
@observable count = 0;
@observable foo ='bar'
}
const store = new Store();
autorun(() => {
console.log('autorun:',store.count,store.foo)
})
store.count = 10
store.foo = 'hello'
//执行结果如下:
autorun: 0 bar
autorun:10 bar
autorun: 10 hello
频繁执行autorun,数据量大时不合理
解决方法:
class store{
@observable count = 0;
@observable foo ='bar'
@action change() {
this.count = 10;
this.foo = 'hello'
}
}
const store = new Store();
autorun(() => {
console.log('autorun:',store.count,store.foo)
})
store.change()
//执行结果如下:
autorun: 0 bar
autorun: 10 hello
强制必须使用action装饰的函数修改observable的值
import { configure } from 'mobx';
configure({
enforceActions: 'observed'
})
when(predicate: () => boolean, effect?: () => void, options?)
when 观察并运行给定的 predicate,直到返回true。 一旦返回 true,给定的 effect 就会被执行,然后 autorunner(自动运行程序) 会被清理。 该函数返回一个清理器以提前取消自动运行程序。
对于以响应式方式来进行处理或者取消,此函数非常有用。 示例:
class MyResource {
constructor() {
when(
// 一旦...
() => !this.isVisible,
// ... 然后
() => this.dispose()
);
}
@computed get isVisible() {
// 标识此项是否可见
}
dispose() {
// 清理
}
}
Copy
class store{
@observable count = 0;
}
const store = new Store();
//当count>100时,只执行一次自定义逻辑
when(
() => {
return store.count > 100;
},
() => {
console.log("when:",store.count);
}
)
用法: reaction(() => data, (data, reaction) => { sideEffect }, options?)。
autorun 的变种,对于如何追踪 observable 赋予了更细粒度的控制。 它接收两个函数参数,第一个(数据 函数)是用来追踪并返回数据作为第二个函数(效果 函数)的输入。 不同于 autorun 的是当创建时效果 函数不会直接运行,只有在数据表达式首次返回一个新值后才会运行。 在执行 效果 函数时访问的任何 observable 都不会被追踪。
reaction 返回一个清理函数。
传入 reaction 的第二个函数(副作用函数)当调用时会接收两个参数。 第一个参数是由 data 函数返回的值。 第二个参数是当前的 reaction,可以用来在执行期间清理 reaction
不同于autorun和when,只有当被观测的数据发生改变时才执行,先执行第一个函数,把第一个函数返回的结果传给第二个函数,data为第一个函数的返回值。第二个参数reaction表示当前函数本身。
reaction(
() => {
//执行一些业务逻辑操作,返回数据给下一个函数使用
return store.count;
},
(data, reaction) => {
console.log("data:",data);
//手动停止当前reaction的监听
reaction.dispose();
}
)
action
mobx推荐将修改被观测变量的行为放在action中。
例子:
import {observable, action} from 'mobx';
class Store {
@observable number = 0;
@action add = () => {
this.number++;
}
}
const newStore = new Store();
newStore.add();
好了回到我们的例子,这个类中有一个add函数,用来将number的值加1,也就是修改了被观测的变量,根据规范,我们要在这里使用action来修饰这个add函数。
就算我把@action去掉,程序还是可以运行。
class Store {
@observable number = 0;
add = () => {
this.number++;
}
}
这是因为现在我们使用的Mobx的非严格模式,如果在严格模式下,就会报错了。
接下来让我们来启用严格模式
import {observable, action, useStrict} from 'mobx';
useStrict(true);
class Store {
@observable number = 0;
@action add = () => {
this.number++;
}
}
const newStore = new Store();
newStore.add();
Mobx里启用严格模式的函数是useStrict,注意和原生JS的"use strict"不是一个东西。
现在再去掉@action就会报错了。
实际开发的时候建议开起严格模式,这样不至于让你在各个地方很轻易地区改变你所需要的值,降低不确定性。
action.bound
action.bound 可以用来自动地将动作绑定到目标对象。,保证this永远指向容器的实例对象
class store{
@observable count = 0;
@observable foo ='bar'
@action.bound change() {
console.log(this)
}
}
const store = new Store();
autorun(() => {
console.log('autorun:',store.count,store.foo)
})
const change = store.change;
change();
//执行结果如下:
Store{change: f, Symbol(mobx did run ...)}
//去掉.bound后执行结果
undefined
注意: *action.bound* 不要和箭头函数一起使用;箭头函数已经是绑定过的并且不能重新绑定。
runInAction
它接收代码块并在(异步的)动作中执行。这对于即时创建和执行动作非常有用,例如在异步过程中。runInAction(f) 是 action(f)() 的语法糖。
class store{
@observable count = 0;
@observable foo ='bar'
@action.bound change() {
console.log(this)
}
}
const store = new Store();
autorun(() => {
console.log('autorun:',store.count,store.foo)
})
runInAction(() => {
store.count = 10;
store.foo = 'hello'
})
//执行结果如下:
autorun: 0 bar
autorun: 10 hello
异步action
action只能影响正在运行的函数,而无法影响当前函数调用的异步操作
开启action严格模式后,不允许在异步的回调函数中对容器状态进行修改。
class store{
@observable count = 0;
@observable foo ='bar'
@action.bound change() {
console.log(this)
}
/*错误用法
@action.bound asyncChange() {
setTimeout(() => {
this.count = 100;
},100)
}
*/
//方法一
@action.bound asyncChange() {
setTimeout(() => {
this.changeCount();
},100)
}
@action.bound changeCount() {
this.count = 20;
}
//方法二
@action.bound asyncChange() {
setTimeout(() => {
//定义一个名称为changeFoo的action函数并立即执行
action('changeFoo',() => {
this.foo = 'hello'
})();
},100)
}
//方法三
@action.bound asyncChange() {
setTimeout(() => {
//定义一个名称为changeFoo的action函数并立即执行
runInAction(() => {
this.foo = 'hello'
});
},100)
}
}
const store = new Store();
store.asyncChange();
Mobx官网实例:
@action createRandomContact() {
this.pendingRequestCount++;
superagent
.get('https://randomuser.me/api/')
.set('Accept', 'application/json')
.end(action("createRandomContact-callback", (error, results) => {
if (error)
console.error(error);
else {
const data = JSON.parse(results.text).results[0];
const contact = new Contact(this, data.dob, data.name, data.login.username, data.picture);
contact.addTag('random-user');
this.contacts.push(contact);
this.pendingRequestCount--;
}
}));
}
重点关注程序的第六行。在end中触发的回调函数,被action给包裹了,这就很好验证了上面加粗的那句话,action无法影响当前函数调用的异步操作,而这个回调毫无疑问是一个异步操作,所以必须再用一个action来包裹住它,这样程序才不会报错。。
使用async function来处理业务,那么我们可以使用runInAction这个API来解决之前的问题。
import {observable, action, useStrict, runInAction} from 'mobx';
useStrict(true);
class Store {
name = '';
load = async () => {
const data = await getData();
runInAction(() => {
this.name = data.name;
});
}
}
在React中,我们一般会把和页面相关的数据放到state中,在需要改变这些数据的时候,我们会去用setState这个方法来进行改变。
先设想一个最简单的场景,页面上有个数字0和一个按钮。点击按钮我要让这个数字增加1,就让我们要用Mobx来处理这个试试。
import React from 'react';
import { observable, useStrict, action } from 'mobx';
import { observer } from 'mobx-react';
useStrict(true);
class MyState {
@observable num = 0;
@action addNum = () => {
this.num++;
};
}
const newState = new MyState();
@observer
export default class App extends React.Component {
render() {
return (
{newState.num}
)
}
}
上例中我们使用了一个MyState类,在这个类中定义了一个被观测的num变量和一个action函数addNum来改变这个num值。
之后我们实例化一个对象,叫做newState,之后在我的React组件中,我只需要用@observer修饰一下组件类,便可以使用这个
newState对象中的值和函数了。
在不使用其它框架、类库的情况下,React要实现跨组件交互这一功能相对有些繁琐。通常我们需要在父组件上定义一个state和一个修改该state的函数。然后把state和这个函数分别传到两个子组件里,在逻辑简单,且子组件很少的时候可能还好,但当业务复杂起来后,这么写就非常繁琐,且难以维护。而用Mobx就可以很好地解决这个问题。来看看以下的例子:
class MyState {
@observable num1 = 0;
@observable num2 = 100;
@action addNum1 = () => {
this.num1 ++;
};
@action addNum2 = () => {
this.num2 ++;
};
@computed get total() {
return this.num1 + this.num2;
}
}
const newState = new MyState();
const AllNum = observer((props) => num1 + num2 = {props.store.total});
const Main = observer((props) => (
num1 = {props.store.num1}
num2 = {props.store.num2}
));
@observer
export default class App extends React.Component {
render() {
return (
);
}
}
有两个子组件,Main和AllNum (均采用无状态函数的方式声明的组件)
在MyState中存放了这些组件要用到的所有状态和函数。
之后只要在父组件需要的地方实例化一个MyState对象,需要用到数据的子组件,只需要将这个实例化的对象通过props传下去就好了。
那如果组件树比较深, 则可以借助React15版本的新特性context来完成。它可以将父组件中的值传递到任意层级深度的子组件中。
useStrict(true);
class MyState {
@observable data = null;
@action initData = async() => {
const data = await getData("xxx");
runInAction("说明一下这个action是干什么的。不写也可以", () => {
this.data = data;
})
};
}
严格模式下,只能在action中修改数据,但是action只能影响到函数当前状态下的情景,也就是说在await之后发生的事情,这个action就修饰不到了,于是我们必须要使用了runInAction。
开启严格模式可以防止数据被任意修改,降低程序的不确定性*
React组件中可以直接添加@observable修饰的变量
@observer
class MyComponent extends React.Component {
state = { a: 0 };
@observable b = 1;
render() {
return(
<div>
{this.state.a}
{this.b}
</div>
)
}
}
在添加@observer后,你的组件会多一个生命周期componentWillReact。当组件内被observable观测的数据改变后,就会触发这个生命周期。
注意setState并不会触发这个生命周期!state中的数据和observable数据并不算是一类。
另外被observable观测数据的修改是同步的,不像setState那样是异步,这点给我们带了很大便利。
如果使用observable来修饰一个Javascript的简单对象,那么其中的所有属性都将变为可观察的,如果其中某个属性是对象或者数组,那么这个属性也将被observable进行观察(递归调用)。
Tips: 简单对象是指不由构造函数创建,而是使用Object作为其原型,或是干脆没有原型的对象。
需要注意,只有对象上已经存在的属性,才能被observable所观测到。
若是当时不存在,后续添加的属性值,则需要使用extendObservable来进行添加。
let observableObject = observable({value: 3222});
extendObservable(observableObject, {
newValue: 2333
});
如果是由构造函数创建的对象,那么必须要再它的构造函数中使用observable或extendObservable来观测对象。
如下所示:
function MyObject(name) {
extendObservable(this, {
name,
});
}
var obj = new MyObject("aaa");
如果对象中的属性是由构造函数创建的对象,那么它也不会被observable给转化。
对象中带有getter修饰的属性会被computed自动转换。
与对象类似,数组同样可以使用observable函数进行转化。
考虑到ES5中原生数组对象中存在一定的限制,所以Mobx将会创建一个类数组对象来代替原始数组。在实际使用中,这些类数组的表现和真正的原生数组极其类似,并且它支持原生数组的所有API,包括数组索引、长度获取等。
但是注意一点,sort和reverse方法返回的是一个新的Observable Arrays,对原本的类数组不会产生影响,这一点和原生数组不一样。
请记住,这个类数组不管和真实的数组有多么相似,它都不是一个真正的原生数组,所以毫无疑问Array.isArray(observable([]))的返回值都是false。
当你需要将这个Observable Arrays转换成真正的数组时,可以使用slice方法创建一个浅拷贝。换句话来说,Array.isArray(observable([]).slice())会返回true。
除了原生数组支持的API外,Observable Arrays还支持以下API:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pa9CT6mO-1664247481872)(https://sponsor.segmentfault.com/lg.php?bannerid=0&campaignid=0&zoneid=25&loc=https%3A%2F%2Fsegmentfault.com%2Fa%2F1190000037619304&referer=https%3A%2F%2Fsegmentfault.com%2Fsearch%3Fq%3Dmobx%26page%3D1&cb=032bde81ac)]
用create-react-app搭建项目,安装了mobx@6.x和mobx-react@6.x,写一个使用store例子时发现依赖store的组件没有重新渲染。
示例:
// mobx@6.0.1
import { action, observable } from 'mobx';
class TestStore {
@observable count = 0;
@action
setValue = <T extends keyof CountStore>(key: T, value: this[T]) => {
this[key] = value;
}
}
export default {
testStore: new TestStore()
}
页面引入
import { inject, observer } from 'mobx-react';
import React from 'react';
enum Oprate {
MINUS = 'MINUS',
PLUS = 'PLUS'
}
function App(props: any) {
const {testStore} = props;
const oprate = (type: Oprate) => {
switch (type) {
case Oprate.MINUS:
testStore.setValue('count', testStore.count - 1);
break;
case Oprate.PLUS:
testStore.setValue('count', testStore.count + 1);
break;
default:
break;
}
}
return (
<div>
<button onClick={() => oprate(Oprate.MINUS)}>--</button>
<span>{testStore?.count}</span>
<button onClick={() => oprate(Oprate.PLUS)}>++</button>
</div>
);
}
export default inject('testStore')(observer(App));
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UKb8n9Me-1664247481873)(https://segmentfault.com/img/bVcH0nD)]
Mobx官网例子如下:
import React from "react"
import ReactDOM from "react-dom"
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react"
// Model the application state.
class Timer {
secondsPassed = 0
constructor() {
makeAutoObservable(this)
}
increase() {
this.secondsPassed += 1
}
reset() {
this.secondsPassed = 0
}
}
const myTimer = new Timer()
// Build a "user interface" that uses the observable state.
const TimerView = observer(({ timer }) => (
<button onClick={() => timer.reset()}>Seconds passed: {timer.secondsPassed}</button>
))
ReactDOM.render(<TimerView timer={myTimer} />, document.body)
// Update the 'Seconds passed: X' text every second.
setInterval(() => {
myTimer.increase()
}, 1000)
无需通过observable和action等修饰器,直接在构造函数中使用makeAutoObservable来实现observable和action修饰器功能,使代码更加简洁。
将上面例子改写一下就可以了
import { makeAutoObservable } from 'mobx';
class TestStore {
constructor() {
makeAutoObservable(this);
}
/*
constructor() {
makeObservable(this, {
count: observable,
setvalue: action,
});
*/
count = 0;
setValue = <T extends keyof CountStore>(key: T, value: this[T]) => {
this[key] = value;
}
}
export default {
testStore: new TestStore()
}