mobx是一个简单、可扩展的状态管理库
什么是状态管理
首先我们来想一下这些问题,什么是状态管理,为什么要使用状态管理?
当我们构建一个前端应用时,组件内部都会有一些状态,使用this.state和setState就可以解决发部分问题。
当应用越来越复杂,出现了兄弟组件之间的通信,跨级组件通信,就需要把所有的状态存储到共同的祖先,通过props层层传递,状态发生变化时,在通过层层的传递,通知祖先组件。在一个大型应用中,状态的管理就会变得异常的复杂混乱。
因此我们需要状态管理库。所有组件的公共状态统一存在一个store中,修改和更新状态也在store中。每个组件中都可以直接获取store中状态,并且修改它。
mobx 原理
通俗一点的解释就是,事件触发actions,更改了store中的数据,然后计算值和UI都会更新。
基本概念
Actions:任何可以改变状态的代码,都是actions。最容易理解的是用户事件(点击按钮)
State:存储的全局状态,数据。
Observable:可以被观察的值,不仅原始值可以被观察,引用值也可以被观察,比如对象,数组。当数据被观察后,使用可观察值的组件,会在观察值变化后,自动重新渲染。
Computed values:相关数据变化可以计算出的值,比如一个可观察数组的长度,就是一个计算值。计算值不是实时更新的,只有使用时,才会更新,不再使用时,会自动被垃圾回收。
Reactions:数据变化是的一些变化,比如发起请求,更新React组件等
observer:react组件增加observer装饰,就会从无状态的组件变成响应式组件,当组件使用了可观察的值,可观察的值变化时组件会自动重新渲染。
自定义 reactions:当unfinishedTodoCount变化时,自动打印日志
1
2
3autorun(() => {
console.log("Tasks left: " + todos.unfinishedTodoCount)
})
mobx使用定义一个可观察的状态
定义了一个可观察状态timer,初始值为0
1
2
3
4
5import {observable} from 'mobx';
var appState = observable({
timer: 0
});
定义响应式组件,触发action改变state
定义了一个响应式组件TimerView,button点击调用resetTimer,页面上 Seconds passed显示当前的timer的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import {observer} from 'mobx-react';
@observer
class TimerView extends React.Component {
render() {
return (
Seconds passed: {this.props.appState.timer}
);
}
onReset() {
this.props.appState.resetTimer();
}
};
ReactDOM.render(, document.body);
更改状态
resetTimer函数将timer置为0,定时函数令timer每秒加1.
1
2
3
4
5
6
7appState.resetTimer = action(function reset() {
appState.timer = 0;
});
setInterval(action(function tick() {
appState.timer += 1;
}), 1000);
这就是mobx工作的过程。先定义一些全局共享的状态,比如timer,并且使其可观察。然后定义响应式的组件,组件中使用了timer,当timer更新时,组件会自动重新渲染。组件中的按钮点击事件,又会触发action,造成state中的timer更新。
这就是mobx的工作流程,action改变state,state改变会更新所有受影响的view。当state更新时,所有依赖这些状态的组件,都会开始细颗粒度的更新。
1actions -------> state --------> views
mobx-react 渲染性能优化原理
我们可以通过使用@observer,将react组件转换成一个监听者(Reactions),这样在被监听的应用状态变量(Observable)有更新时,react组件就会重新渲染。如下代码中,TodoItemModel中的应用状态变量有更新时,TodoItem UI会重新渲染:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class TodoItemModel {
id
@observable title;
@observable completed;
......
}
@observer
class TodoItem extends React.Component {
.....
render() {
//UI logic code ...
let todo = this.props.todo;
let title = todo.title;
let complete = todo.complete;
return (
....
);
}
}
var todoItem = new TodoItemModel();
todoItem.title = '';
在使用react的过程中,我们绕不开渲染性能优化问题,因为默认情况下react组件的shouldComponentUpdate函数会一直返回true,这回导致所有的组件都会进行耗时的虚拟DOM比较。在使用redux作为react的逻辑层框架时,我们可以使用经典的PureComponent+ShallowCompare的方式进行渲染性能优化,那么在使用mobx作为react的store时,我们该如何进行渲染性能优化呢?
(1). 默认的基本渲染性能优化
通过分析源代码发现,在使用@observer将react组件转换成一个监听者(Reactions)后,mobx会为react组件提供一个精确的、细粒度的shouldComponentUpdate函数:
1
2
3
4
5
6
7
8
9shouldComponentUpdate: function(nextProps, nextState) {
......
// update on any state changes (as is the default)
if (this.state !== nextState) {
return true;
}
// update if props are shallowly not equal
return isObjectShallowModified(this.props, nextProps);
}
借助于mobx框架对Observable变量引用的跟踪和依赖收集,mobx能够精确地得到react组件对Observable变量的依赖图谱,然后再用经典的ShallowCompare实现细粒度的shouldComponentUpdate函数,以达到100%无浪费render。这一切都是自动完成地,fantastic!使用mobx后,我们再也无需手动写shouldComponentUpdate函数了。
(2). 使用transaction进行高级渲染性能优化
mobx在带来便利性的同时,也可能引入新的问题。上文反复提到:mobx会记录监听者(Reactions)中对Observable变量的引用,通过引用在运行时动态地构建依赖图谱,从而实现精确地、细粒度地更新。
如果一个React组件依赖于多个应用状态(Observable)变量,而一次操作需要更新多个应用状态(Observable)变量时,React组件就会进行多次渲染。在有些场景下,更新地粒度过细,也不是我们希望看到的场景。比如如下代码,调用TodoItemModel的reset方法设置了两个应用状态变量,会导致对应的UI组件重新渲染两次:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35//todoItem UI组件,依赖于应用状态变量completed和title
@observer
class TodoItem extends React.Component {
...
render() {
//UI logic code ...
let todo = this.props.todo;
let title = todo.title;
let completed = todo.complete;
return (
......
);
}
}
class TodoItemModel {
id;
@observable title;
@observable completed;
......
reset() {
/*
分别设置title和completed值,
会触发两次todoItem UI组件的重新渲染
*/
this.completed = false;
this.title= '';
}
......
}
var todoItem = new TodoItemModel();
//调用reset将触发两次TodoItem UI组件的重新渲染
todoItem.reset();
在语义层面,有时我们需要将多个应用状态(Observable)的更新,视为一次操作,并希望只触发一次监听者(Reactions)的动作(UI更新、网络请求等)。为此mobx提供了transaction功能,可以将对多个应用状态(Observable)的更新封装成一个事务,只有在事务执行完成后,才会触发一次对应的监听者(Reactions)。这就使得我们对组件的渲染有更精细化的控制。比如如下代码,使用transaction后,对应用状态(Observable)的两次更新只会触发一次UI的更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class TodoItemModel {
id;
@observable title;
@observable completed;
......
reset() {
/*
使用transaction设置title和completed值,
只会在函数调用结束后触发一次todoItem UI组件的重新渲染
*/
transaction(()=>{
this.completed = false;
this.title= '';
})
}
......
}
var todoItem = new TodoItemModel();
//调用reset只触发一次TodoItem UI组件的重新渲染
todoItem.reset();
最佳实践
1. 所有更改state都用action
使用action后,可以清楚的看出,哪些代码可以更改可观察的变量。
使用action,方便调试工具给出更多的信息
使用transaction可以将多个应用状态(Observable)的更新视为一次操作,并只触发一次监听者(Reactions)的动作(UI更新、网络请求等),避免多次重复渲染。action中封装了transaction,多次改变@observable变量时,只会重新渲染一次,提高了性能。
1
2
3
4
5
6
7
8
9
10
11
12
13@autobind
class TodoItemModel {
id;
@observable title;
@observable completed;
//使用action后,reset函数执行完成后,才会触发一次其监听者
@action
reset() {
this.completed = false;
this.title= '';
}
}
假设某个组件使用了reset函数,虽然对2个可观察对象completed和title都改变了,但是只会触发一下。
2. 所有的action都放在store中
把所有的状态改变都放在store中,不要散落在各个组件中。在排查问题时,各个组件都可以修改状态,很难定位是哪儿的问题。数据的变化都放在store中,看起来更清晰,也更好维护。
3. 使用compute属性
compute的值是延迟更新的,只有在使用的时候,才会自动计算。不再使用时,它会自动被垃圾回收。因此使用compute的值可以提高性能。
4. 组件使用@observer
如果一个组件需要使用@observable变量(应用状态),就应该使用@observer修饰符。
@observer可以将 React 组件转变成响应式组件,确保任何组件渲染中使用的数据变化时都可以强制刷新组件。
借助于精确的依赖分析,mobx可以得出组件对@observable变量(应用状态)的依赖图谱,对使用@observer进行标记的组件,实现精准的shouldComponentUpdate函数,保证组件100%无浪费渲染。如果我们不对子组件使用@observer,我们就放弃了mobx对React组件的默认性能优化。
5. 延迟对象属性地解引用
本质上,mobx使用ES5的新特性Object.defineProperty对属性设置set和get来实现对对象属性变动的监听和依赖跟踪。只有在真正需要使用Observable变量的组件中,再对其解引用,才能使得mobx构建出正确的依赖图谱。从而使得mobx能够精确的更新对特定属性有依赖的Reactions。
比如对于todolist,正确的写法是在子组件中才对需要使用的属性进行解引用:
1
2
3
4
5
6class TodoItemModel {
id;
@observable title;
@observable completed;
}
let todoItem = new TodoItemModel();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27@observer
class MainSection extends React.Component {
.......
render() {
......
return (
......
{todos.map(
(todo) =>
)}
......
);
}
}
@observer
class TodoItem extends React.Component {
......
render() {
let todo = this.props.todo;
let title = todo.title;
let complete = todo.complete;
return(
......
)
}
}
如果在父组件中对属性过早的解引用,而向子组件传递原始类型的变量,则导致mobx无法搜集到子组件对应用状态的依赖,如下代码是错误的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class MainSection extends React.Component {
.......
render() {
......
return (
......
{todos.map(
(todo) =>
)}
......
);
}
}
@observer
class TodoItem extends React.Component {
......
render() {
let title = this.props.title;
let complete = this.props.complete;
return(
......
)
}
}