系列
- 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();
}
}