天天看点

mobx 6 + typescript 实现React状态管理 (1) -- mobx 基本使用及 Api 详解

系列

  • mobx 基本使用及 Api 详解
  • mobx 封装
  • mobx 封装后常见场景使用案例
  • 源码

说明

重要依赖版本
  • mobx: 6.3.0 | mobx-react: 7.1.0 | react: 17.0.2

1. MobX 概念及结合 React 的简单使用

  • State:数据
  • Derivations: 派生,是一些由 state 计算而来的数据
  • Reactions: 反应,与派生类似,但是不会生成数据,而是会执行某些任务,一般用来DOM更新或者网络请求
  • Actions:行动,所有对 state 的改动都应在这里进行,Action 中触发的 state 改动又会自动的触发 Derivations 及 Reactions
  • 没有了类似 Redux 容器组件、UI 组件的区分,所有组件都是可以是 observer

MobX 结合 React 使用

  • 通过 mobx 创建 “可观察”的数据(Observable)
  • 借助 “mobx-react” 将 react 组件变成 观察者,观察者也就是 mobx 概念中 Reactions 反应与组件的结合产物
  • React 组件中某些操作触发 action 执行,由此就完成了一个闭环

为什么不通过 装饰器来使用 MobX ?

  • 装饰器 Decorator 是一种与 类 Class 相关的语法,用来注释或修改类和方法,许多面向对象的语言都有这项功能,例如: Python, 但是在 JS 中截止当前(2021.04.23)还是提案阶段,因此其用法可能会有改变
  • 装饰器是一种函数,写成

    @ + 函数名

    它可以放在类和类方法的定义前面;它不仅能够增加代码的可读性,清晰的表达意图,而且提供了一种方便的手段,增加或者修改类的功能;不过由于我们的ES6+ 代码都需要经过 Babel 编译,使用 Decorator 后编译出的代码体积会很大
  • 在 Mobx 6 以前,一直提倡的是装饰器语法,但是自从 Mobx 6 开始不再建议使用装饰器,以便最大程度的贴近 JS 标准

MobX + React 的简单使用示例

1. 创建“可观察”的数据(Observable)
  • 将数据变成可观察的有 makeObservable, makeAutoObservable, observable 三个 Api
  • 三个Api 比较起来 makeAutoObservable 是最方便的,但是,makeAutoObservable 处理的”类“是不可以具有 super subClass 等继承关系的,这对封装 store 会造成一些麻烦
// 1. 使用 makeObservable(target, annotations?) 
class Store {
  public users: { name: string; tel: number }[] = [];

  constructor() {
    makeObservable(this, {
      users: observable, // 定义 users 是可观察的
      userNum: computed, // 定义 userNum 为派生值(或者计算值)
      increaseUser: action // 定义 increaseUser 为 action
    });
  }

  public get userNum(): number {
    return this.users.length;
  }

  public increaseUser = (user: { name: string; tel: number }) => {
    this.users.push(user);
  };
}

// 2. 使用更智能的 makeAutoObservable(target, overrides?)
class Store {
  public users: { name: string; tel: number }[] = [];

  constructor() {
    // 与 makeObservable 相比,会自动推断属性, 不再需要枚举
    makeAutoObservable(this);
  }

  public get userNum(): number {
    return this.users.length;
  }

  public increaseUser = (user: { name: string; tel: number }) => {
    this.users.push(user);
  };

  // 异步 Action
  public getUsers = () => {
    const getApi = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve([
          { name: 'xiaozhang', tel: 19029 },
          { name: 'bnaiding', tel: 17723 },
        ]);
      }, 3000);
    });

    getApi.then((users: UserStore['users']) => {
      // 异步 Action 中的”可观察数据“更新需要通过 mobx.runInAction 包裹
      runInAction(() => {
        this.users = users;
      });
    });
  };
}

// 3. 使用 observable 将对象变成可观察的
observable({
  users: [],
  get userNum(): number {
    return this.users.length;
  },
  increaseUser(user: { name: string; tel: number }) {
    this.users.push(user);
  };
})
           
2. 将 React 组件变成“观察者”
  • “mobx-react” 会提供一个名为 observer 的 HOC 帮助我们将组件变成观察者
  • 成为观察者的组件,只有在当前组件内使用的 “可观察数据” 发生改变时才会重新 render 组件,否则组件不做更新,从而提高性能
  • 可观察数据一般是一个对象,而且这个对象的值可能变化,但是引用地址不会改变
  • observer 相当于帮我们做了 pureComponent\memo,而且控制 render 的更加精细
  • 没有使用 “可观察数据” 的组件(使用指的是调用 store.xxx | store[‘xxx’] | const {xxx} = store) 不需要使用 observer(会在控制台 warning),应改为使用 React.memo | React.PureComponent 来增强性能
// 创建 store
const originalStore = new Store();

// 入口组件 传入 originalStore
const Origin: React.FC = () => {
  console.log('original container render');
  return (
    <div className={UserLess.container}>
      <div className={UserLess.content}>
        <p>userNum: {originalStore.userNum}</p>
        <UserCreateForm originalStore={originalStore} />
        <UserList originalStore={originalStore} />
      </div>
    </div>
  );
};

// 这里用 observer 包裹,因为这个组件调用了 “可观察数据”
export default observer(Origin);

interface Props {
  originalStore: TOriginalStore;
}

// 创建 User 的组件,使用 originalStore 的方法
const UserCreateForm: React.FC<Props> = (props: Props) => {
  const { originalStore } = props;
  const onFinish = (values: { name: string; tel: number }) => {
    originalStore.increaseUser(values);
  };

  console.log('render user create Form');
  return (
    <header className={UserLess.header}>
      <Form onFinish={onFinish}>
        <Form.Item label="姓名" name="name">
          <Input />
        </Form.Item>
        <Form.Item label="电话" name="tel">
          <InputNumber />
        </Form.Item>
        <Form.Item label="module" name="module">
          <Button type="primary" htmlType="submit">
            create
          </Button>
        </Form.Item>
      </Form>
    </header>
  );
};

// 没有使用 observer 包裹,因为组件实现中仅仅调用了 action 而没有使用“可观察数据”
// 使用 React.memo 是为了帮助组件在传入 props 不变的情况下 减少 render 
export default React.memo(UserCreateForm);


// user list 展示组件
const UserList: React.FC<Props> = (props: Props) => {
  const { originalStore } = props;
  console.log('render User List');
  return (
    <ul className={UserLess.main}>
      <li>
        <span>姓名</span>
        <span>电话</span>
      </li>
      {originalStore.users.map((user: { name: string; tel: number }) => {
        return (
          <li key={user.name}>
            <span>{user.name}</span>
            <span>{user.tel}</span>
          </li>
        );
      })}
    </ul>
  );
};

// 使用 observer 包裹
export default observer(UserList);


/**
  通过表单创建一个 user 数据会发现 UserCreateForm 组件没有触发 render(因为这个组件中没有 ”可观察数据“ 的”调用“,例如 store.xxx store['xxx'] const {xxx} = store;)
  original container render
  render User List
*/
           

2. MobX 常用 API 参考

  • 为了在使用时减少类似 import {} from ‘mobx’ 这样的操作,我们会将 mobx 作为全局变量引入(webpack.ProvidePlugin)
  • 同时 mobx 提供的 TS 类型我们会在 global.d.ts 中做下处理后,一些常用的类型会挂载到 Mobx 上。
  • 综上:如果要使用 mobx 提供的方法则 – mobx.isObservableArray() ; 如果要使用 mobx 提供的类型则 – Mobx.TObservableArray (没有的话在 global.d.ts 中追加)
  • MobX 全部 API
// global.d.ts
import * as lodash from 'lodash';
import * as MobX from 'mobx';
import {IObservableArray, IObservable, ObservableMap, ObservableSet} from 'mobx';

declare global {
  const mobx: typeof MobX;

  namespace Mobx {
    export type TObservableArray<T> = IObservableArray<T>;
    export type TObservable = IObservable;
    export type TObservableMap<K, V> = ObservableMap<K, V>;
    export type TObservableSet<T> = ObservableSet<T>;
  }
}
           

1. 用于创建 Observable 可观察数据的 Api

/**
    1. makeObservable, makeAutoObservable 都可以用来将对象转化为 "可观察对象"
    他们的 api 大同小异,其中 makeObservalble 需要通过第二个参数,来确定对象的属性怎么转化成 Observerable
    makeAutoObservable 也有第二个参数,是选填的,与 makeObservalble 的写法一样,
    不过 makeAutoObservable 会自动判断,第二个参数只是用作覆盖自动判断的结果。
*/
const a = {
  value: 'a value',
  get valStr() {
    return `${this.value}--`;
  },
  reset() {
    this.value = 'a value';
  },
};
mobx.makeObservable(
  a, // 原始对象
  {
    value: mobx.observable, // makeObservable 需要手动给每个对象的属性确定类型
    valStr: mobx.computed, // 如果使用 makeAutoObservable
    reset: mobx.action,
  },
  {
    deep: true, // 是否深层递归转化 default: true
    autoBind: false, // action 是否自动 bind default: false
    name: '调试用',  // 起个名,方便调试
  },
);

/**
    2. observable 具有与 makeAutoObservable 相同的 参数配置,但是它不会将源对象转变为 Observable
    而是创建一个新的 Observable 对象, 同时创建的对象是通过 proxy 包裹的,可以追加新的属性,
    make(Auto)Observable 不能直接追加新的属性,追加后,新的属性不具有响应能力
    理论上 make(Auto)Observable 修改后的对象是非 Proxy 对象,处理速度上会更快,
    因此 建议使用 make(Auto)Observable 并提前确定好属性
*/
const b = mobx.observable(
  a, // a 不会变
  {
    value: mobx.observable,
    valStr: mobx.computed,
    reset: mobx.action,
  },
  {
    deep: true,
    autoBind: true,
    name: '调试用',
  },
);

/**
    3. extendObservable 用来给已有对象,追加其他 Obser vable 属性
*/
const c = mobx.extendObservable(b, { b: 'ccc' });

console.log(c.b);

/**
    4. observable.box 用来将简单类型变成 Observable, 
    对其他 Api 来讲,只有引用类型可以转换为 Observable, 这个 Api 专门用来处理简单类型数据 例如 string | number    
*/
const a = mobx.observable.box('string');

mobx.autorun(() => {
  console.log(a.get());
});

a.set('new string');

/**
    5. mobx-react 提供 useLocalObservable 可以代替 useState 来给 函数式组件提供 ”可观察的“ state
*/
const timer = useLocalObservable(() => ({
    secondsPassed: 0,
    increaseTimer() {
        this.secondsPassed++
    }
}))

/**
    6. 其他 observable.array observable.map 等 Api 查看文档即可
*/
           

2. 与触发 action 相关的 Api

理论上建议所有 mobx state 的修改都通过 action 来完成(虽然直接修改也会有反应,但是这不是推荐的做法,而且控制台会给出警告⚠️
/**
    1. 通常来讲在创建 Observable Store 时,对象的方法都会被理解为 action ,可以直接使用
    xxxStore.xxxAction 来触发
*/
React.useEffect(() => {
  mobxApiStore.fetchUsers();
}, []);

/**
    2. 可以借助 mobx.action 直接触发 mobx state 改动
*/
const act = mobx.action(() => {
  mobxApiStore.users.clear(); // 可以直接操作环境中的 observable 数据
});

const act1 = mobx.action((state: TGlobalStore['mobxApiStore']) => {
  state.users.clear(); // observable 数据也可以是通过参数传入的
});

const act2 = () => {
  mobxApiStore.users.clear(); // Warning 这种是非正确的写法
};
return (
    <div>
          <Button onClick={() => act()}>click with action</Button>
          <Button onClick={() => act1(mobxApiStore)}>click with action</Button>
          <Button onClick={() => act2()}>click without action</Button>
    </div>
)

/**
    3. 借助 runInAction 在异步的 action 中触发 mobx state 更新
    据官网中解释,理论上异步的进程在 mobx 中不需要特殊处理。因为不管在何时因此的 mobx state 改动都会触发响应
    但是在异步的进程中更新 state 的操作还是要被标记成 action,使用 runInAction 只是其中一种方案
   runInAction 会创建一个立即执行函数,立即执行你对 state 更改的操作,并触发响应
*/
class MobxApiStore {
  public users = ([] as unknown) as Mobx.TObservableArray<{ name: string; tel: number }>;

  public fetchUsers = () => {
    const getApi = new Promise((resolve, reject) => {
      setTimeout(() => {resolve([...])}, 3000);
    });

    getApi.then((users: MobxApiStore['users']) => {
      // 异步的更新 store 操作需要通过 runInAction 包裹
      mobx.runInAction(() => {
        this.users = users;
      });
    });
  };
}

export default MobxApiStore;

           

3. 响应 mobx state 更改的 Api

Action 触发 mobx state 更新后,我们需要根据 state 更改的结果来做 DOM 更新,触发事件等操作,

即:mobx 概念 中的 Derivations(派生)与 Reactions(反应)部分

更多细节查看 文档 及 什么样的操作会触发响应

/**
    1. computed 计算值或者派生值,是根据 state 变化而变化的值,通常写在 mobx Store 中以 Getter 形式存在
    更多使用查看 https://mobx.js.org/computeds.html
*/
class MobxApiStore {
  public users = ([] as unknown) as Mobx.TObservableArray<{ name: string; tel: number }>;
  
  // computed
  public get userNum(): number {
    return this.users.length;
  }
}

// 也可以直接同 mobx.computed api 直接创建一个计算值(注意需要.get()才能拿到值)
mobx.computed(() => remoteStore.currentRemote.scope).get()

/**
    2. autorun 用来自动观察可观察对象的变化并运行,创建 autorun 时会运行一次,
    后续会对其所包含的 observable 或 computed 做出响应
*/

const a = mobx.observable.box('string');
mobx.autorun(() => {
  console.log(a.get()); // 会执行两次 定义 autorun 时一次,改变时一次
});
a.set('new string');

/**
    3. reaction 与 autorun 的作用类似,都是追踪可观察对象的变化然后运行,
    但是 reaction 可以更精确的控制效果触发的时间,它的参数构成与 autorun 不同, 
    接收两个回调函数作为参数,其中第一个参数用来调用可观察对象,并计算出需要处理的”值“,
    第二个参数接收第一个回调函数 return 的值,并作出响应。
    注意:与 autorun 不同的是,第二个回调函数不会在创建时立即调用,只会在第一个函数返回新值时触发
*/
const a = mobx.observable.box('string');

mobx.reaction(
  () => a.get(),
  (value, prevVal) => {
    console.log(value, prevVal); // 仅会在 a.get() 更改后才会执行
  },
);

a.set('a string');

/**
    4. when 与 reaction 类似,也接收两个回调函数作为参数,
    不同的是 when 的第一个函数参数只作为第二个函数参数执行与否的判断条件,而不会将返回值传下去
    如果不传第二个参数,那么 when() 执行后会返回一个 Promise,可以方便结合 async await 使用
    注意:when 在第一个函数返回 true 后就会被销毁,第二个函数只会执行一次    
*/
const a = mobx.observable.box(false);
mobx
  .when(() => a.get())
  .then(() => {
    console.log('after a change'); // 虽然多次对 a 进行更改但是,when 只会执行一次
  });
 
mobx.runInAction(() => {
  a.set(true);
  a.set(false);
  a.set(true);
});

/**
    5. autorun reaction when 三者都会持续监听”可观察数据“的变化,直到这些可观察数据被垃圾回收,
    一般来讲我们不会对 ”可观察数据“做特定的垃圾回收,因此为了不让这三者开启后一直运行,造成内存泄漏
    现给他们三者都提供了 dispose 销毁程序,即 三者调用后的返回值, 此外 autorun 与 reaction, 函数体中的最后一个参数也可以用来 dispose
*/
const dispose = mobx.autorun((reaction) => {
  reaction.dispose();
});
dispose();
mobx.reaction(
  () => '',
  (value, prevVal, reaction) => {
    reaction.dispose();
  },
);

/**
    6. autorun reaction when 三者的最后一个参数用来进一步微调行为
*/
mobx.reaction(
  () => remoteStore.currentRemote.scope,
  (val) => {
    console.log('update', val);
  },
  {
    fireImmediately: false, // 立即执行 (reaction)
    delay: 0, // 第二个参数函数延迟执行毫秒数 (autorun reaction)
    // timeout: 0, // when 等待的截止时间,过时则 reject
  },
);

/**
    7. mobx-react 的 observer 用来在可观察对象变化时,通过高阶组件来重新渲染其包裹的组件
    仅在组件中有对 可观察对象读取行为时才有在 可观察对象 变化时的重新渲染的能力
    读取行为包括 (xxxStore.xxx xxxStore['xxx'] const {xxx} = xxxStore)
*/
import { observer } from 'mobx-react';
import originalStore from './OriginalStore';

const Origin: React.FC = () => {
  React.useEffect(() => {
    originalStore.fetchUsers();
  }, []);
  console.log('original container render');
  return (
     <p>userNum: {originalStore.userNum}</p>
  );
};

export default observer(Origin);

/**
    8. 如果我们组件中有在组件的回调函数中用到 ”可观察数据“ 那么需要 <Observer> 组件来协助
    否则不会达到预期效果    
*/
const renderUserName = React.useCallback(
  () => (
    <Observer>{() => <div>{mobxApiStore.users.map((u) => u.name).join(', ')}</div>}</Observer>
  ),
  [],
);
// React.useCallback 可以使得传入参数 renderUserName 指针地址是不变的, 减少频繁 render
<UserCreateForm mobxApiStore={mobxApiStore} renderUserName={renderUserName} />
           

4. 数据计算与判断相关 Api

/**
    1. toJS() 用于将 ”可观察数据“ 转化为普通 JS 数据类型
*/
const a = mobx.observable(['aaa', 'bbb']);
console.log(a, mobx.toJS(a)); // Proxy { <target>: (2) […], <handler>: {…} } Array [ "aaa", "bbb" ]

/**
    2. observable 数据类型判断
*/
const arr = mobx.observable(['aaa', 'bbb']);
const obj = mobx.observable({ a: 'aaa' });
const box = mobx.observable.box('string');
const map = mobx.observable.map({ a: 'aaa' });
const set = mobx.observable.set(['aaa', 'bbb']);
const computed = mobx.computed(() => arr.join(', '));

console.log('isObservable', mobx.isObservable(arr), mobx.isObservable([]));
console.log('isObservableArray', mobx.isObservableArray(arr), mobx.isObservableArray(['aaa']));
console.log('isObservableObject', mobx.isObservableObject(obj), mobx.isObservableObject({ a: 'aaa' }));
console.log('isObservableMap', mobx.isObservableMap(map), mobx.isObservableMap(new Map()));
console.log('isObservableSet', mobx.isObservableSet(set), mobx.isObservableSet(new Set()));
console.log('isBoxedObservable', mobx.isBoxedObservable(box));
console.log('isComputed', mobx.isComputed(computed), mobx.isComputed(obj));

/**
    2.1 深层比较两个 observable 数据的值是否相等
*/
const obj3 = mobx.observable({ a: { aa: 'aaa' } });
const obj4 = mobx.observable({ a: { aa: 'aaa' } });
console.log('comparer', mobx.comparer.structural(obj3, obj4)); // ture

/**
    2.2 可观察的对象,怎么获取 key值
*/
const originObj = {key: 'sss'};
const observableObj = mobx.observable({ a: 'aaa' });

// 对标原生对象的一些使用,获取 observableObject key数组 value数组等
console.log(mobx.keys(observableObj), Object.keys(originObj));
console.log(mobx.values(observableObj), Object.values(originObj));
console.log(mobx.entries(observableObj), Object.entries(originObj));
mobx.autorun(() => {
  // 注意在 set 前 'b' 还不存在,但是这里不会报错,
  console.log(mobx.get(observableObj, 'b'));
});
// 给可观察数据设置值,可以设置没有初始化的key,同样会引起观察者更新
mobx.set(observableObj, 'b', 'bbb');
// 判断 observableObject 是否有某个 key
console.log(mobx.has(observableObj, 'b'));
// 移除 observableObject 的某一项
mobx.remove(observableObj, 'a');

/**
    3. 数组,对象、Map、Set 被转换成 observable 后有那些 api 上的变化?
    这些内置对象的基本 Api 都还保留,除了类型判断尽量使用上边 api 外,observable 还追加了一些 api
*/

// 3.1 数组追加三个方法 clear() replace() remove() 禁用 _.isArray | Array.isArray
const arr = mobx.observable(['aaa', 'bbb']);
arr.clear(); // 删除数组中所有当前条目
console.log(mobx.toJS(arr));
arr.replace(['kkk']); // 替换所有现有条目为 ...
console.log(mobx.toJS(arr));
const arr2 = mobx.observable([
  { a: 'aaa', del: false },
  { b: 'bbb', del: true },
]);
arr2.forEach((item) => {
  if (item.del) {
   // 删除指定项目, 返回 boolean 表示是否有指定值被删除
    console.log(arr2.remove(item));
  }
});
console.log(mobx.toJS(arr2));

// 3.2 Map 追加三个方法 toJSON() merge() replace() 
const map2 = mobx.observable.map({ a: { aa: 'aaa' } });
console.log(map2.toJSON()); // 返回此Map的浅表普通对象表示形式,(没啥用)
console.log(mobx.toJS(map2)); // 返回 ES6 Map
map2.merge({ a: { bb: 'bbb' } }); // 合并 Map 可以传普通对象或者 Map
console.log(mobx.toJS(map2)); // 例中: a 的值会被覆盖
map2.replace({ n: 'nnn' }); // 替换 Map 全部内容
console.log(mobx.toJS(map2));

/**
    4. 如果 Store 中的数据是以上类型,然后在业务代码中使用 内置对象被追加的 api 
    则会发现有 TS 检查报错,这是因为store 中的类型声明或者 TS 类型推断出错 
    这时我们可以重新声明数据的类型,如下(如果不使用追加的方法,完全可以不做处理)
*/
class MobxApiStore {
  private readonly global: TGlobalStore;
  /**
      ❌错误案例:
      注意: 如下这种方式定义类型不再建议使用,因为定义为 Observable 类型后,
      普通的 JS 处理会受到 TS 类型检查的报错影响, 例如在 store 内 users = [] 会报错(通常不建议直接赋值,主要是因为我给 mobx 封装后有个 api updateStore() 会被报错)
  */ 
  public users = ([] as unknown) as Mobx.TObservableArray<{ name: string; tel: number }>; // ❌
  public userSet = new Set() as Mobx.TObservableSet<string>; // ❌
  public userMap = new Map() as Mobx.TObservableMap<string, any>; // ❌
  
  /**
      ✔️正确案例:
      在每次需要用到追加 api 的位置去定义类型
  */
  
  public users: { name: string; tel: number }[] = []; // 数据定义为普通的数据类型

  public someAction = () => {
      // 使用 追加 api 时,强制类型为 Mobx.TObservableArray
      (this.users as Mobx.TObservableArray<FuncComStore['users'][number]>).clear();
  }
}