Mobx
关于Mobx
1.关于Mobx和Redux
Mobx和Redux一样,是React状态管理比较流行的解决方案之一。和Redux的原理不一样的是,Mobx早期参考了vue,使用了响应式编程,而React+Mobx的选型就是一个复杂版的Vue。
Mobx工作流程:
Redux工作流程:
Mobx的流程相比Redux而言,非常简洁也很容易上手,性能也相对更好。
react以及redux的各种hooks本身的特性以及hooks带来的各种新的状态管理库确实写起来非常顺手,但是hooks不能完全代替class组件,在大型项目里,Mobx依然是个不错的选择。
2.关于Mobx版本
mobx6版本之前大量使用了装饰器语法,但是在6.x之后抛弃了装饰器语法。目前最高版本就是6.x版本。
原因是当时装饰器语法还没有纳入到ES标准之中,标准化的过程还需要很长时间,而且未来制定的标准可能与现在的方案有所不同。处于兼容性的考虑,Mobx6中放弃了装饰器语法。
目前在ES7中已经加入了装饰器语法,装饰器语法确实非常简洁。
Mobx响应式对象的原理参考了VUE,在6.x之后使用了Proxy代理,在6.0之前使用的是Object.defineProperty API,Mobx在6.x之后提供了降级方案,可以在configure配置中设定。
Mobx5最好配合React16.x使用,搭配React17及以上会报错。
3.Mobx的核心概念
概念 | 用法 |
---|---|
observable state | 被mobx跟踪的状态 |
computed | 根据应用程序状态派生的新值,计算值 |
action | 允许修改状态的方法,严格模式下只有action方法被允许修改状态 |
observer | 视图组件必须通过observer方法包裹 |
三个常用的库
库 | 功能 |
---|---|
mobx | 核心库 |
mobx-react | 支持函数式组件以及class组件 |
mobx-react-lite | mobx-react的轻量版,只支持hooks |
二 Mobx6.x的用法
1. 基于Mobx6.x以及react的useContext的例子。
这里用的是mobx核心库以及mobx-react,项目结构如下:
Counter.ts
import { makeAutoObservable } from 'mobx';
class Counter {
count: number = 0;
constructor() {
makeAutoObservable(this);
}
increase() {
this.count += 1;
}
decrease() {
this.count -= 1;
}
get double() {
return this.count * 2;
}
}
export default Counter;
stores/index.ts
import React from 'react';
import Counter from './Counter';
import Themes from './Themes';
const stores = React.createContext({
counter: new Counter(),
themes: new Themes(),
});
export default stores;
hooks/useStores.ts
import React from 'react';
import stores from '../stores';
const useStores = () => React.useContext(stores);
export default useStores;
Pane.tsx
import React, { useEffect } from 'react';
import { Row, Col, Button } from 'antd';
import { PlusOutlined, MinusOutlined } from '@ant-design/icons';
import { observer } from 'mobx-react-lite';
import useStores from './hooks/useStores';
type PaneProps = React.HTMLProps<HTMLDivElement> & {
name?: string;
}
const Pane: React.FC<PaneProps> = ({ name, ...props }) => {
const stores = useStores();
return (
<div {...props}>
{name && <h2>{name}</h2>}
<Row align="middle">
<Col span="4">Count</Col>
<Col span="4">{stores.counter.count}</Col>
<Col>
<Button
type="text"
icon={<PlusOutlined />}
onClick={() => stores.counter.increase()}
/>
<Button
type="text"
icon={<MinusOutlined />}
onClick={() => stores.counter.decrease()}
/>
<Button
type="text"
onClick={() => stores.counter.calcCount()}
>异步</Button>
</Col>
</Row>
</div>
);
};
Pane.defaultProps = { name: undefined };
export default observer(Pane);
2.常用的API
以上的代码只涉及到两个API,Mobx的makeAutoObservable和mobx-react-lite的observer。
Mobx通过makeAutoObservable方法来构造响应式对象,传入的对象属性会通过Proxy进行代理。
makeAutoObservable是makeObservable的智能版,能自动推断所有属性,规则如下:
- 所有属性都成为observable;
- 所有getters都成为computed;
- 所有setters都成为action;
- 所有prototype中的functions都成为autoAction;
除了通过makeAutoObservable简化处理之外,Mobx还有其他api可以逐个定义。
Mobx常用的API:
- observable
- action
- computed
- makeObservable
stores/Themes.ts
import { action, makeObservable, observable, computed } from 'mobx';
export default class Themes {
color = 'red';
constructor() {
makeObservable(this, {
color: observable,
changeColor: action,
isDefaultTheme: computed,
});
}
changeColor() {
this.color = `#${Math.random()
.toString(16)
.slice(-6)}`;
}
get isDefaultTheme() {
return this.color === 'red';
}
}
注意:
(1)cpmputed计算值是有缓存的。
(2)如果修改行为时直接修改状态而不是通过action修改,那么在非严格模式下控制台会有提示信息,严格模式下编译报错。
3.异步方法
Vuex中将状态的修改分为mutation和action,同步修改放到mutation中,异步操作放到action中。
在Mobx中,同步或者异步操作都可以放在action里,但是在异步操作修改属性时,需要将赋值操作放到runInAction中。如果不调用runInAction,也可以用本身存在的action。
在Counter.ts中添加一个异步方法calcCount:
import { makeAutoObservable, runInAction } from 'mobx';
class Counter {
count: number = 0;
constructor() {
makeAutoObservable(this);
}
increase() {
this.count += 1;
}
decrease() {
this.count -= 1;
}
get double() {
return this.count * 2;
}
setCount(count: number) {
this.count = count;
}
async calcCount() {
const count: number = await new Promise(resolve => {
setTimeout(() => {
resolve(10);
}, 2000)
})
// 用runInAction或者调用内部setCount方法都可以
// runInAction(() => {
// this.count = count;
// })
this.setCount(count)
}
}
export default Counter;
4.Mobx提供的其他一些API
(1) autoRun监听对象变更
autoRun接收一个函数作为参数,函数用来执行副作用。
当函数内部使用使用的observable state,computed发生变化时函数会运行,初始运行autoRun方法时函数函数也会运行一次。
import React, { useEffect } from 'react';
import { Row, Col, Button } from 'antd';
import { PlusOutlined, MinusOutlined } from '@ant-design/icons';
import { observer } from 'mobx-react-lite';
import { autorun } from 'mobx';
import useStores from './hooks/useStores';
type PaneProps = React.HTMLProps<HTMLDivElement> & {
name?: string;
}
const Pane: React.FC<PaneProps> = ({ name, ...props }) => {
const stores = useStores();
useEffect(() => {
autorun(() => {
console.log('counter--', stores.counter.count)
})
}, [])
return (
<div {...props}>
{name && <h2>{name}</h2>}
<Row align="middle">
<Col span="4">Count</Col>
<Col span="4">{stores.counter.count}</Col>
<Col>
<Button
type="text"
icon={<PlusOutlined />}
onClick={() => stores.counter.increase()}
/>
<Button
type="text"
icon={<MinusOutlined />}
onClick={() => stores.counter.decrease()}
/>
<Button
type="text"
onClick={() => stores.counter.calcCount()}
>异步</Button>
</Col>
</Row>
</div>
);
};
Pane.defaultProps = { name: undefined };
export default observer(Pane);
注意:
- 这里在autoRun外包了useEffect,这是因为每次检测到变化时,autoRun自己会多执行一次。
- 对于基本数据类型,mobx跟踪不到复制后的值;引用数据类型,只要引用地址不发生变化,Mobx就可以跟踪。
(2)reaction, when
除了autoRun之外,Mobx还提供了更精细的监听方法。
reaction(
() => stores.counter.count,
(value, prevValue) => {
console.log('diff', value - prevValue)
}
);
reaction的用法类似于vue中的watch。
when(() => stores.counter.count > 5, () => {
console.log(stores.counter.count)
})
三 Mobx装饰器写法
tsconfig.json中开启装饰器的使用:
"experimentalDecorators": true,
"useDefineForClassFields": true,
引入babel进行转码:
npm install @babel/plugin-proposal-decorators
babel.config.js中添加这个插件:
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
新建Counter1.ts
import { makeObservable, observable, action, computed } from 'mobx';
class Counter1 {
@observable count: number = 0;
constructor() {
makeObservable(this);
}
@action increase() {
this.count += 1;
}
@action decrease() {
this.count -= 1;
}
@computed get double() {
return this.count * 2;
}
@action setCount(count: number) {
this.count = count;
}
@action async calcCount() {
const count: number = await new Promise(resolve => {
setTimeout(() => {
resolve(10);
}, 2000)
})
this.setCount(count)
}
}
export default Counter1;
mobx-react-lite的useObserver api的用法
import React, { useEffect } from 'react';
import { Row, Col, Button } from 'antd';
import { PlusOutlined, MinusOutlined } from '@ant-design/icons';
import { useObserver } from 'mobx-react-lite';
import { autorun } from 'mobx';
import useStores from './hooks/useStores';
type PaneProps = React.HTMLProps<HTMLDivElement> & {
name?: string;
}
const Pane: React.FC<PaneProps> = ({ name, ...props }) => {
const stores = useStores();
useEffect(() => {
autorun(() => {
console.log('counter--', stores.counter.count)
})
}, [])
return useObserver(() =>
<div {...props}>
{name && <h2>{name}</h2>}
<Row align="middle">
<Col span="4">Count</Col>
<Col span="4">{stores.counter.count}</Col>
<Col>
<Button
type="text"
icon={<PlusOutlined />}
onClick={() => stores.counter.increase()}
/>
<Button
type="text"
icon={<MinusOutlined />}
onClick={() => stores.counter.decrease()}
/>
<Button
type="text"
onClick={() => stores.counter.calcCount()}
>异步</Button>
</Col>
</Row>
</div>
)
};
Pane.defaultProps = { name: undefined };
export default Pane;
useObserver是包裹着render中的内容。