Mobx库应用于项目的可行性研究
1.概述
Mobx是一个运用透明的函数式响应编程,实现状态管理的库 具体的一些介绍还是前往这里看吧:mobx.nodejs.cn/README.html 在这里就只是记录一下将Mobx库引入到cocos项目的可能性
2.研究引入Mobx库的原因
一般开发一个功能或者一个系统的时候都会有一个Model类,一个或多个View类,它们一个负责处理数据,一个负责处理界面业务逻辑。所以在业务开发过程中,总是会有一个这样的需求:我需要监听某个数据结构内的某一个特定变量,当这个变量发生改变时,我需要实时地去处理某些业务。 这个功能很容易实现,使用简单的发布-订阅模式就可以了,流程大概如下:
kotlin
class 数据类 {
//...省略单例实例化
public 变量A: number = 0;
public 变量B: {Name: string, Age: number} = {Name: '张三', Age: 18};
public onDataChange(data: any): void {
if (this.变量A !== data.变量A) {
this.变量A = data.变量A;
发布消息(自定义消息Id1, this.变量A);
}
}
public onDataChange2(data: any): void {
this.变量B.Name = data.Name;
this.变量B.Age = data.Age;
发布消息(自定义消息Id2, this.变量B);
}
}
class 界面类 {
onLoad(): void {
订阅消息(自定义消息Id1, 业务接口1, this);
订阅消息(自定义消息Id2, 业务接口2, this);
}
业务接口1(变量A): void {
// .....处理数据类.I.变量A相关的业务
}
业务接口2(变量B): void {
// .....处理数据类.I.变量B相关的业务
}
onDestroy(): void {
注销消息(自定义消息Id1, 业务接口, this);
注销消息(自定义消息Id2, 业务接口, this);
}
}
一般来说都是这样的一个流程,一直以来我也都是这么做,直到有一天接触到了Mobx,了解了一下Mobx的响应式编程,觉得也许可以将Mobx引入到项目中来,简化上面的流程。 使用Mobx来实现上述的功能,大概是以下流程:
typescript
import {makeAutoObservable, autorun} from "mobx"
class 数据类 {
//...省略单例实例化
public 变量A: number = 0;
public 变量B: {Name: string, Age: number} = {Name: '张三', Age: 18};
constructor() {
makeAutoObservable(this);
}
public onDataChange(data: any): void {
this.变量A = data.变量A;
}
public onDataChange2(data: any): void {
this.变量B.Name = data.Name;
this.变量B.Age = data.Age;
}
}
class 界面类 {
onLoad(): void {
autorun(() => {
// ...与数据类.I.变量A相关的业务
})
autorun(() => {
// ...与数据类.I.变量B相关的业务
})
}
}
随后只要数据类里的变量A或变量B有变化,界面类的对应的autorun就会自动执行,突出一个简单。
3.主要概念介绍
1.可观察状态
想要使用Mobx的响应编程,首要的就是为自己想要观察(或者说监听)的对象创建可观察状态。方法则是使用Mobx的makeObservable和makeAutoObservable,为每个属性指定一个注解,这里只展开说以下三个,具体的定义还是看官网,我这里就简单概括一下:
- observable-就是定义了一个对象可以被观察,拥有了状态;
- action-用于标记一个函数方法,这个方法主要用于修改被observable观察了的对象;
- computed-则用于标记getter方法,用于缓存结果输出,当结果发生变化时,会自动调用。 接下来简单说一下makeObservable和makeAutoObservable的用法和区别:
- makeAutoObservable是makeObservable的加强版,两者都是需要在构造函数中使用,第一个参数传this。
- makeObservable需要自己手动将每一个需要观察的对象罗列出来,作为参数传递给第二位参数,而makeAutoObservable则是自动为推断this中的所有属性和对象,包括函数方法,自动为他们添加注解。推断规则为:
- 所有拥有的属性都为 observable
- 所有的 getter 方法都为 computed
- 所有的 setter 方法都为 action
- 所有的function都为action
举个🌰:
typescript
class Model {
private _name: string = '';
private _age: number = 18;
private _data: {value: number, max: number} = {value: 10, max: 20};
constructor() {
// 使用makeObservable
makeObservable(this, {
_name: observable,
_age: observable,
_data: observable,
name: computed,
age: computed,
setDataValue: action.bound
})
// 使用makeAutoObservable
makeAutoObservable(this, {autoBind: true});// 结束了,是的你没看错,就是这么简单
}
public get name(): string {
return this._name;
}
public set name(v: string) {
this._name = v;
}
public get age(): number {
public this._age;
}
public set age(v: number ) {
this._age = v;
}
public setDataValue(v): number {
this._data.value = v;
}
}
那么问题来了,无脑用makeAutoObservable不就可以了吗?这就得说到makeAutoObservable的一个限制,它不能用在 具有父类 或 拥有子类 的类,也就是说,如果你这个类是继承别的类的,或者你这个类是有被其他类继承的,那么你就不能使用makeAutoObservable。
2.action-操作
对于Mobx来说,如果你为一个对象创建了可观察状态,那么当你需要改变该对象时,Mobx会要求你在action标注的函数内改变,例如上面代码块中的 setDataValue。
3.computed-计算
可以通过computed来创建计算值,它会充当一个缓存点,只有当计算值发生了变化,才会触发计算。 简单的🌰
typescript
class Model {
//.....省略一大堆代码
public value1: number = 0;
public value2: number = 2;
// 使用computed为getTotalNum添加注解
public getTotalNum() {
return this.value1 + this.value2;
}
}
autorun(() => {
console.log(Model.I.getTotalNum());
})
首先autorun是什么暂且不说,只需要知道,这样的写法就会有一个效果:当getTotalNum()的计算结果发生变化,那么autorun内的逻辑就会被自动触发执行。 此时如果改变 value2,由于value1=0,所以计算结果始终是0,autorun就不会被执行。当改变value1,计算结果改变了。autorun被执行。
4.reaction-反应
反应简单来说就是对观察对象发生变化后,自动执行的逻辑,比如上面代码里的autorun。
- autorun(effect: (reaction) => void, options?) autorun 函数接受一个函数,该函数应在每次观察到任何变化时运行。 当你创建 autorun 本身时,它也会运行一次。 它仅响应可观察状态的变化,即你注释为 observable 或 computed 的内容。
- reaction(() => value, (value, previousValue, reaction) => { sideEffect }, options?) reaction 与 autorun 类似,但对跟踪哪些可观察量提供了更细粒度的控制。 它需要两个函数: 第一个数据函数被跟踪并返回用作第二个效果函数的输入的数据。
- when(predicate: () => boolean, effect?: () => void, options?) when需要两个函数,观察并运行给定的谓词函数(第一个函数),当第一个函数结果为true时,执行第二个函数。
4.使用
在上面的介绍里,我们知道了为对象添加观察状态有两种方法,makeAutoObservable无可否认是最简单的,但是却有不能用在子类或有子类的父类上,那么在使用Mobx的过程中就出现一个问题,如果我这个类要观察的对象很多,那么我岂不是要手动写观察写到手软。 为了偷懒,还是简单封装一下吧,下面是我封装的一个接口,目前还没真正去验证是否能在复杂的类中使用。
typescript
/** 遍历类实例对象中定义的属性、对象和方法成员
* @param target 类实例对象
*/
function myMakeAutoObservable(target: any): { [key: string]: any } {
// 拿到所有成员
let proto = Object.getPrototypeOf(target);
// 拿到所有定义的属性成员
let proto1 = Object.keys(target);
// 拿到所有定义的函数方法
let proto2 = Object.getOwnPropertyDescriptors(proto);
// 梳理所有定义的成员,进行Mobx观察状态添加
const observableMap: { [key: string]: any } = {};
let key: string = '';
for(let i = 0; i < proto1.length; i++) {
key = proto1[i];
if (target[key] !== undefined && target[key] !== null) {
// 为属性和对象添加observable注解
observableMap[key] = observable;
}
}
for (let key in proto2) {
const e = proto2[key];
if (key === 'constructor') {
// 跳过构造函数
continue;
}
if (target[key] == undefined || target[key] == null) {
continue;
}
if (e.value) {
if (typeof e.value === 'function') {
// 函数方法添加action
observableMap[key] = action.bound;
}
}
if (e.get) {
// getter添加computed
observableMap[key] = computed;
}
}
return observableMap;
}
// 父类
class ModelBase {
constructor() {
}
/** 用于自动注解类中定义的成员和属性 */
public makeAutoObservable(target: any): void {
const map = myMakeAutoObservable(target);
Mobx.makeObservable(target, map);
}
}
class MyModel extends ModelBase {
// ..... 省略了 .....
constructor() {
super();
this.makeAutoObservable(this);
}
// ..... 也省略了 ......
}
大概就这么用了。
5.注意点
目前对于Mobx的了解都还只是皮毛,有一些注意点需要记录一下:
- Mobx要为属性和对象创建观察状态,也就是observable,那么属性和对象一定要是已经进行过初始化、实例化的。也就是说,如果你定义了一个对象:public myNum: number = null; public myObject = null;那么myNum 和 myObject 都是没办法观察的。
- Mobx的反应处理,在不需要用到的时候,也要记得销毁。比如在界面内使用了autorun,那么界面销毁时,也要把相关的反应销毁掉,避免造成内存泄漏。目前我觉得比较简单的方法,就是在界面的基类进行接口封装,自动销毁。简单来说,就是这样:
typescript
import { autorun, reaction, when } from "mobx";
class ViewBase extends cc.Component {
/** 保存mobx反应disposer */
private _mobxDisposers: any[] = [];
/**
* 封装autorun
* @param runFn 自动执行的函数
*/
public autorun(runFn: () => any) {
const disposer = autorun(runFn);
this._mobxDisposers.push(disposer);
}
/**
* 封装reaction
* @param expression 跟踪观察对象并返回结果,作为第二个参数runFun的输入
* @param runFn 自动执行的函数
*/
public reaction(expression: () => any, runFn: (args: any) => any) {
const disposer = reaction(expression, runFn)
this._mobxDisposers.push(disposer);
}
/**
* 封装when
* @param predicate 跟踪观察对象,判断结果
* @param runFn 如果第一个参数结果为true,则自动执行
*/
public when(predicate: () => boolean, runFn: () => any) {
const disposer = when(predicate, runFn)
this._mobxDisposers.push(disposer);
}
/** 界面销毁时自动调用 */
public onDestroy() {
if (this._mobxDisposers && this._mobxDisposers.length > 0) {
this._mobxDisposers.forEach(disposer => {
disposer && disposer();
});
this._mobxDisposers = null;
}
}
}