从Decorator到Mobx

最近在开发一款视频编辑器,其中就用到了Mobx作为状态管理工具。Mobx中很重要的概念例如可观察(observable)的状态,可计算(computed)的值都用到了decorator(当然在使用Mobx时可以不用)。Decorator作为ES7引入的新特性,用于给类/属性添加新行为。对于不少初学者而言,可能对其并不是很了解,所以在这里从装饰器开始,聊聊我对Decorator和Mobx的理解。如果你正在学习Mobx,希望能对你快速上手Mobx能有所帮助。

先说装饰器(Decorator)

装饰器是ES7中引入的,其目的在于修改类/方法的行为。例如我们可以在不修改“类”的情况下为其增加新的功能。

例如:我们定义了一个学生“类”,其中有nameage两个属性,以及showInfo一个方法。

1
2
3
4
5
6
7
8
9
class Student {
constructor(name, age) {
this.name = name;
this.age = age;
}
showInfo = () => {
console.log(`name:${this.name}, age: ${this.age}`)
}
}

如果此时我们想为这个类添加一个属性school用于标明学校,,在不修改“类”的情况下,我们可以使用装饰器这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function addSchool(target) {
target.prototype.school = 'SDU';
}
@addId
class Student {
// ...
}

/**
@decorator
class A{}
等价于
A = decorator(A);
*/

let limoer = new Student('limoer', 21);
console.log(limoer.school); // > SDU

addSchool()给Student“类”的原型对象上添加了一个属性,现在所有实例都可以取到school这个属性。

更深入一步,上面看到用于装饰的函数只接收一个目标“类”作为参数,如果我们有多个参数的话,可以写成高阶函数的形式(即返回一个函数)。同样是上面的例子,现在学校由参数指定,我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
function addSchool(school_name) {
return function(target) {
target.prototype.school = school_name;
}
}

@addSchool('CQMU')
class Student {
// ...
}

let lin = new Student('lin', 20);
console.log(lin.school); // > CQMU

装饰器不但可以装饰“类”,也可以对方法(…属性)进行修饰,使用的方式类似于对“类”的修饰,不过用于修饰的函数接收三个参数,target将要被修饰的对象, name被修饰的属性名, descriptor被修饰的属性的描述对象(ES5中详细介绍过)。 写一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

function showCount(target, name, descriptor) {
let prev = descriptor.value;
descriptor.value = function() {
console.log('count:' + StudentList.list.length);
prev.call(null, arguments);
}
return descriptor;
}

class StudentList {

static list = ['limoer', 'lin'];

@showCount
showNames () {
console.log(StudentList.list.join(' '));
}
}

let list = new StudentList();
list.showNames(); // count:2 \n limoer lin

上面的代码给StudentList类的showNames方法添加了打印数量的功能,并且是在不改变原有“类”结构的情况下。

说明,在现有的浏览器环境和Node都不能运行上面的代码(暂不支持装饰器),如果想运行的话,可以借用babel 并且使用相关插件(babel-plugin-transform-decorators-legacy)的前提下进行compile,之后就可以进行了。推荐开发过程中webpack和babel结合使用,效果更佳!

好了,关于Decorator简单介绍到此到一段落,更多的相关知识请自行发掘和学习。接下来,是时候了解并使用Mobx了!

Mobx?想说爱你不容易!

在文章最开头谈到我在最近的学习开发中使用了Mobx作为状态管理工具,最主要的原因是其相比Redux,学习和快速上手成本的确消了很多,并且它足够简单。但是在后来的开发过程中,虽然其可以没有redux中action,也不存在reducer,更是告别了单一而庞大的store,我们可以定义多个state用于保存状态,让每个状态或者是每个类属性添加注解,让其编程可观察的状态,而为了能够自动的更新值,我们可以通过使用computed这个装饰器或者autorun函数来完成。可是,在使用过程中,定义多少个状态,每个状态的结构又是如何,等等等等,都困扰着我,远没有使用redux来得清晰和直观。这也许是因为我对mobx目前刚好达到基本使用的程度,并没有深入的了解。基于此,接下来,我只谈谈Mobx入门,至于该如何优雅的使用,请自行摸索。

几个概念

  1. 可观察的状态

这也许是Mobx最基础也是最重要的概念了。我们可以使用Mobx提供的observable装饰器,让基本的数据结构(数组、对象、原始值等)变成可观察的。使用的方式如下:

1
2
3
4
5
6
7
8
9
10
let TimeState = observable({
currentTime: Date.now()
})
TimeState.set("currentTime", new Date().toString());

class AppState {
@observable list = ['limoer', 'lin'];
}
let state = new AppState();
console.log(state.list.length); // > 2

好了,最简单的例子就是这样,我们使用ES5和ES6 decorator的方式分别创建了两个state,第一个state我们适应装饰器让一个对象(Map)变得可观察,而第二个我们则是对一个“类”属性(为一个数组)进行了修饰,让其变成可观察的。

这里值得注意的是,如果一个数据结构变得可观察,那么其类型也会发生改变,例如我们让一个数据变得可观察,此时其已经变成了一个 Observable Array, 这是一种Mobx定义的数据结构,拥有其独特的API,此时使用Array.isArray(state.list)讲返回false,因为Observable Array 并不是一种数组类型。

好了,当看到这里,你是否有这样一个疑问:让一个数据结构变得可观察,其作用到底在哪里呢?其实很简单,我们都知道Mobx是React的小伙伴,其目的是在于替换React本身的state,我们都知道对于React而言,如果一旦state发生改变,就将导致页面更新并且重新渲染,基于此,让数据结构变得可观察,其目的是在于当被观察的数据发生改变,React也能做出相应的更新和重绘操作等,并且,这样的重绘是经过Mobx优化的,只进行必要的重绘来增加性能!

  1. 可计算值

可计算值是通过现有状态和其它可计算值派生出来的值。这很好理解,我们在使用React的时候,往往要通过state衍生出很多的值,例如如果state的一部分是一个数组,那么我们通过衍生得到的数组长度就是一个计算值,并且在Mobx中,一旦可观察的state或者其他computed value 发生改变,可计算值就会重新计算。其实,在实际的React项目中,我们在很多地方都使用到了计算值。

还是上面AppState的例子,现在我们给其增加一个计算值,

1
2
3
4
5
6
7
8
9
10
class AppState {
@observable list = ['limoer', 'lin'];
@computed get count() {
return this.list.length;
}
}
let state = new AppState();
console.log(state.count); // > 2
state.list.push('lindo');
console.log(state.count); // > 3

count是一个计算值,一旦list发生变化,其就会自动重新计算,可以保证,count的值每次都是最新的,并且都是等于list数组的长度。

  1. autorun

其作用和函数名一样好理解,其会自动执行;autorun其本身是一个响应式函数,其使用到的依赖关系state/computed value等一旦发生改变,其就会自动执行一次,效果和计算值类似,但是计算值和autorun的应用场景是不一样的,computed value通常会产生一个新值而autorun达到某种目的而不产生新值,例如生成日志,处理网络请求等。
还是上面的例子,我们继续扩展:

1
2
3
4
class AppState {
// ...省略前面的代码
let logcount = autorun(() => {console.log('count: ' + this.count)});
}

这里我们在autorun中使用了computed value, 一旦发生count改变,就会自动打印出新的count值;当然,初始化state实例对象的时候,就会先执行一次。

  1. action

动作是用来修改状态的。并且只应该对修改状态的函数使用action,要使用动作很简单,使用@action修饰一个函数或者使用action(fn),把要修饰的函数作为参数即可。继续上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13

class AppState {
// 省略上面的代码
@action.bound
addOne(name) {
this.list.push(name);
}
// 或者
@action
addOne = (name) => {
this.list.push(name);
}
}

上面我们定义了一个函数,用于向列表中添加一个姓名。请注意,ES6 class的写法无法自动绑定到对象,所以使用`@action.bound` 或者是使用ES6中引入的箭头函数(推荐)。

与React使用

  1. observer
    observer是由mobx-react包(需独立安装)提供的用于让组件变成响应式组件的decorator。官方文档中写到:它用 mobx.autorun 包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件。
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
import React, { Component } from 'react';
import { render } from 'react-dom';
// 其余依赖省略
@observer
class NameList extends Component {
addUser = (e) => {
e.preventDefault();
if(this.uname.value){
this.props.appstate.addOne(this.uname.value);
}else{
console.log('must input user name!');
}
}
render() {
return <div>
<ul>
{
this.props.appstate.list.map((index, name) => {
return <li key={index + 10}>{name}</li>
})
}
</ul>
<div>
<p>当前用户人数:{this.props.appstate.count}</p>
<label for="uname">姓名</label>
<input type="text" name="uname" ref={(ref) => this.uname = ref}/>
<button onClick={this.addUser}>+</button>
</div>
</div>
}
}

render(<NameList appstate={appstate} />, document.getElementById('app'));

上面是一个响应式组件的例子,结合了上面定义的状态,我们可以查看所有的姓名、数量,并且可以通过点击按钮来改变state。其实observer对非响应式组件仍然有效,同样是上面的例子:

1
2
3
4
5
6
7
const List = observer(({appstate}) => {
return <ul>
appstate.list.map((index, name) => {
return <li key={index + 19}>{name}</li>
})
</ul>
})

好了,对于observer的介绍就告一段落,更多的Mobx和React连接的方式,以及Mobx提供的生命钩子函数等相关知识你可以查看官方文档来了解。

尾巴

自从放了暑假回了家,效率下降特别多,在学校的时候以为回家可以安心学习,到了家才知道一切都变了,该做的事情还没做,还有更多的知识要学习。所以,早早回学校也许是一个不错的选择!所以再过几天,就要启程回学校了,在最后一年里,期待所有的努力都没有白费,期待一个新(好)的开始!

分享到 评论