Mobx库应用于项目的可行性研究

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中的所有属性和对象,包括函数方法,自动为他们添加注解。推断规则为:
  1. 所有拥有的属性都为 observable
  2. 所有的 getter 方法都为 computed
  3. 所有的 setter 方法都为 action
  4. 所有的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。

  1. autorun(effect: (reaction) => void, options?) autorun 函数接受一个函数,该函数应在每次观察到任何变化时运行。 当你创建 autorun 本身时,它也会运行一次。 它仅响应可观察状态的变化,即你注释为 observable 或 computed 的内容。
  2. reaction(() => value, (value, previousValue, reaction) => { sideEffect }, options?) reaction 与 autorun 类似,但对跟踪哪些可观察量提供了更细粒度的控制。 它需要两个函数: 第一个数据函数被跟踪并返回用作第二个效果函数的输入的数据。
  3. 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的了解都还只是皮毛,有一些注意点需要记录一下:

  1. Mobx要为属性和对象创建观察状态,也就是observable,那么属性和对象一定要是已经进行过初始化、实例化的。也就是说,如果你定义了一个对象:public myNum: number = null; public myObject = null;那么myNum 和 myObject 都是没办法观察的。
  2. 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;
      }
  }
}
相关推荐
ZZZCY200310 分钟前
路由策略与路由控制实验
前端·网络
shawya_void19 分钟前
javaweb-day01-html和css初识
前端·css·html
khatung20 分钟前
React——useReducer
前端·javascript·vscode·react.js·前端框架·1024程序员节
思考的橙子22 分钟前
CSS之3D转换
前端·css·3d
木子七1 小时前
vue3-setup中使用响应式
前端·vue
廖子默1 小时前
提供html2canvas+jsPDF将HTML页面以A4纸方式导出为PDF后,内容分页时存在截断的解决思路
前端·pdf·html
Moment1 小时前
毕业半年,终于拥有了两个近 500 star 的开源项目了 🤭🤭🤭
前端·后端·开源
光影少年1 小时前
react和vue图片懒加载及实现原理
前端·vue.js·react.js
AndyGoWei2 小时前
react react-router-dom history 实现原理,看这篇就够了
前端·javascript·react.js