一、Mobx机制介绍:
Mobx 是简单、可扩展的状态管理,当应用状态更新时,任何源自应用状态的东西都将自动地获得。React 和 MobX 是一对强力组合。React 通过提供机制把应用状态转换为可渲染组件树并对其进行渲染。而 MobX 提供机制来存储和更新应用状态供 React 使用。
Mobx 的运行机制如下图:
首先从左往右看,事件触发了 Actions,Actions 作为唯一修改 State 的方式,修改了 State,State 的修改更新了计算值 Computed,计算值的改变引起了 Reactions 的改变,导致了 UI 的改变,Reactions 可以经过事件调用 Actions。
二、开发环境
安装 mobx 和 React 绑定库 mobx-react:
npm install mobx mobx-react --save
要启用 ESNext 的装饰器 (可选), 参见下面:
- https://unpkg.com/mobx/lib/mobx.umd.js
- mobx - Libraries - cdnjs - The #1 free and open source CDN built to make life easier for developers
安装修饰符修饰器插件
// 修饰符的插件
npm install --save-dev babel-plugin-transform-decorators-legacy
// 装饰器的一个插件
npm install @babel/plugin-proposal-decorators
配置babelrc文件,在根目录创建一个.babelrc文件(存在.babelrc文件的直接进行配置), 输入以下内容:
{
"plugins":[
[
"@babel/plugin-proposal-decorators",
{
"legacy":true
}
],
[
"@babel/plugin-proposal-class-properties",
{
"loose":true
}
]
],
"presets":[
"react-app"
]
}
三、常用API介绍及用法
1、@observable (定义变量状态):observable
用于定义可观察状态,观测的数据可以是数字、字符串、数组、对象等。
使用@observable定义的变量是可以被修改的,如果是常量或者不可再被修改的变量就可以不用@observable修饰
例:从mobx中引入observable, import {observable} from 'mobx'; 使用如下:
@observable count: number = 0;
@observable tool: string = "";
sex: string = 'nan';
@observable classCurrentId: string = '';
@observable classDataList: any = [];
注意:被 observable
观测数据的修改是同步的,不像 setState 那样是异步。
2、@observer (观察者):被observer修饰的组件,将会根据组件内使用到的被observable修饰的state的变化而自动重新渲染
一般用在修饰类class前面,内部有可观察的变量的话(通过@observable定义的),添加@observer,即可实时观察数据变化,重新render
例:从mobx-react中引入observer,import {observer} from "mobx-react";
如下: 只要store中的count变化了,下面这个Timer组件就会重新render,重新更新,这就是@observer的用途
import store from './mobx/store';//引入store
@observer
class Timer extends React.Component {
render() {
return (
<div>{store.count}</div>
)
}
};
3、reaction (监听):组件中通过reaction监听store数据变化,变化之后在组件内部作出相关响应
可以监听一个store变量,也可以监听多个store变量,如下所示
监听一个的例子:
// 监听classCurrentId的变化,newClassId为最新的数据
reaction(() => store.mobTest.classCurrentId, (newClassId) => {
//dosomething
})
// 监听classDataListLength变化, newClassDataList为最新的数据
reaction(() => store.mobTest.classDataListLength, (newClassDataList) => {
//dosomething
});
销毁:
let disposeOrgChange = reaction(
() => this.global.currentOrgInfo.id,
(id) => {
if (id) {
//doSomething
}
}
);
在组建销毁的时候执行
this.disposeOrgChange && this.disposeOrgChange();
监听多个变量例子:
/**
监听screenMode和ClassType.type的变化,下面newData为最新数据,可以通过
newData.mode, newData.type获取最细的数据
/*
reaction(() => ({ mode: store.home.screenMode, type: store.ClassType.type }), newData => {
//doSomething
})
4、toJS():将观察者对象转换成正真定义的格式,如将观察者数组转换成数组,将观察者对象转换成对象
如:在store中定义了一个可修改的变量为数组格式:@observable answerList: any[] = []; 我们在获取最新answerList数据的时候,拿到的不是真正意义上的数组,是被观察着数组,这时候我们必须通过toJS转换一下就可以了
例子:
// answerList为store中定义的被观察的数组,使用reaction监听的时候,通过toJS转换成真正的数组
reaction(() => toJS(store.answer.answerList), newValue => {
//doSomething
})
5、@inject (注入):注入store中的方法和变量,然后组件里就可以直接通过this.props.xxx的方式使用其他store中的变量或方法
使用inject注入要注意,需要在跟组件将App组件使用Provider包裹,如下
import { Provider } from 'mobx-react'
//store为定义的mobx引入的store文件
class App extends Component{
render(){
return(
<Provider store={store}>
<ToDoApp/>
</Provider>
)
}
}
组件中使用案例:
import store from '../mobx/store'; //直接引入store
interface IProps {
store?: any; //通过App传入的store
}
interface IState {
}
@inject('store');//注入所有store
class PokeButton extends React.Component<IProps, IState> {
//doSomething 组件里面就可以通过 this.props.store.xxx获取任意store数据
}
6、@computed (计算属性):使用@computed修饰的方法,内部只要有一个变量变化,就会重新计算,会在依赖的状态发生变化时会重新运行
注意点:
computed 值会被缓存:每当读取 computed 值时,如果其依赖的状态或其他 computed 值未发生变化,则使用上次的缓存结果,以减少计算开销。
computed 值会惰性计算:只有 computed 值被使用时才重新计算值。反之,即使 computed 值依赖的状态发生了变化,但是它暂时没有被使用,那么它不会重新计算。
例子:
//store中定义
@computed get classDataListLength() {
return this.classDataList.length;
}
使用:classDataListLength变化时执行
// 监听mobx 班级选中列表元素变化
reaction(() => store.mobTest.classDataListLength, (newClassDataList) => {
//doSomething
});
7、autorun(自定义反应):用于定义响应函数,并在定义时立即执行一次。 以后,每当依赖状态发生变化时,autorun
自动重新运行。
当你想创建一个响应式函数,而该函数本身永远不会有观察者时,可以使用 mobx.autorun
。 这通常是当你需要从反应式代码桥接到命令式代码的情况,例如打印日志、持久化或者更新UI的代码。 当使用 autorun
时,所提供的函数总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发。 相比之下,computed(function)
创建的函数只有当它有自己的观察者时才会重新计算,否则它的值会被认为是不相关的。 经验法则:如果你有一个函数应该自动运行,但不会产生一个新的值,请使用autorun
。 其余情况都应该使用 computed
。 Autoruns 是关于 启动效果 (initiating effects) 的 ,而不是产生新的值。 如果字符串作为第一个参数传递给 autorun
,它将被用作调试名。
传递给 autorun 的函数在调用后将接收一个参数,即当前 reaction(autorun),可用于在执行期间清理 autorun。
就像@observer 装饰器/函数,autorun 只会观察在执行提供的函数时所使用的数据。
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();
numbers.push(5);
// 不会再输出任何值。`sum` 不会再重新计算。
8、@action(动作):用于定义状态修改操作
常用案例:只有通过@action才可以修改store数据
import {observable, action} from 'mobx';
class AnswerData {
@observable currentState: string = '';
@observable micList: any[] = [];
@observable teaStatus: string = 'set';
@observable timer: number = 0;
timerId: any = null;
startTimer = () => {
this.timerId = setInterval(() => {
this.timer += 1
}, 1000);
}
@action changeState (state: string) {
this.currentState = state;
}
@action setList (l: any[]) {
this.micList = l;
this.startTimer();
}
@action teaQuizEnd = () => {
this.teaStatus = 'reStart';
}
}
export const answer = new AnswerData();
异步操作处理:
@action
只会对当前运行的函数做出反应,对于一些不在当前函数中调用的回调是无效的,例如 setTimeout
回调、Promise
的 then
或 async
语句。这些回调如果修改了状态,也应该用 action
包裹起来。以下方法可处理该问题:
修改状态的语句用 runInAction
包裹起来,runInAction包裹执行完成之后再修改
class Store {
@observable githubProjects = []
@observable state = "pending" // "pending" / "done" / "error"
@action
async fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
const projects = await fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
// await 之后,再次修改状态需要动作:
runInAction(() => {
this.state = "done"
this.githubProjects = filteredProjects
})
} catch (error) {
runInAction(() => {
this.state = "error"
})
}
}
}
用法:
action(fn)
action(name, fn)
@action classMethod() {}
@action(name) classMethod () {}
@action boundClassMethod = (args) => { body }
@action(name) boundClassMethod = (args) => { body }
@action.bound classMethod() {}
@action.bound(function() {})
注意:action只能影响正在运行的函数,而无法影响当前函数调用的异步操作
@action createRandomContact () {
superagent.get('https://randomuser.me/api/').set('Accept', 'application/json')
.end(action("createRandomContact-callback", (error: any, results: any) => {
if (error) {
console.error(error);
} else {
console.log(results)
}
}));
}
在 end 中触发的回调函数,被 action 给包裹了,这就很好验证了上面的那句话,action 无法影响当前函数调用的异步操作,而这个回调毫无疑问是一个异步操作,所以必须再用一个 action 来包裹住它,这样程序才不会报错。
如果你使用 async function
来处理业务,那么我们可以使用 runInAction
这个 API 来解决问题。
import {observable, action, runInAction} from 'mobx';
class Store {
@observable name = '';
@action load = async () => {
const data = await getData();
runInAction(() => {
this.name = data.name;
});
}
}
你可以把 runInAction
有点类似 action(fn)()
的语法糖,调用后,这个 action
方法会立刻执行。
交流
1、QQ群:可添加qq群共同进阶学习: 进军全栈工程师疑难解 群号: 856402057
2、公众号:公众号「进军全栈攻城狮」 ,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!