前言
之前在学习react 的状态管理时当然第一个学到的就是redux,关于redux 如果有兴趣或是还不清楚,可以先去看看 浅析React Redux 的概念以及使用。而本篇的主角是MobX,这篇会介绍关于MobX 的一些概念以及基本用法,当然也会讲到跟redux 的区别,包括优势选型等比较。
正文
MobX 其实跟redux 一样也是一个用于做全局状态管理的一个工具,不过经过下面的介绍你就会发现MobX 比redux 方便简单很多,所以其实MobX 也蛮流行的。我会在介绍完MobX 后再统一说说跟redux 的对比。下面一样以todo 应用作为场景说说MobX 的实现,废话不多说进入MobX 吧~
MobX 准备工作
首先,要在react 应用中使用MobX 我们当然要先装MobX 的包:
// npm
npm install mobx mobx-react
// yarn
yarn add mobx mobx-react
MobX 基本使用
其实MobX 的概念很简单,首先当然还是先要设计我们todo 的store。我们当然也是先建一个store 文件夹存放状态管理相关的所有代码。
商店+行动
如果对redux 有了解的人会知道,在redux 中,redux 只有一个统一的大的store,而具体怎么处理,是根据你分发的action 中的 type
进行不同的reducer 处理,然后返回新的state。
不过在MobX 不是这样的。MobX 不是单一数据源,在MobX 可以有多个store,我们可以根据不同的业务将状态分成各自的store,其实在某种意义上更直观一点。在MobX 中,我们借住ES6 的 class
实现store,来看看代码:
// /store/todoStore.ts
import { makeAutoObservable } from "mobx";
export interface Todo {
id: number;
text: string;
done: boolean;
}
const removeTodo = (todos: Todo[], id: number): Todo[] =>
todos.filter((todo) => todo.id !== id);
const addTodo = (todos: Todo[], text: string): Todo[] => [
...todos,
{
id: Math.max(0, Math.max(...todos.map(({ id }) => id))) + 1,
text,
done: false,
},
];
// Todo 總數據源
class TodoStore {
todos: Todo[] = [];
newTodo: string = "";
constructor() {
makeAutoObservable(this);
}
addTodo() {
this.todos = addTodo(this.todos, this.newTodo);
this.newTodo = "";
}
removeTodo(id: number) {
this.todos = removeTodo(this.todos, id);
}
loadTodo(url: string) {
fetch(url)
.then((res) => res.json())
.then((data) => (this.todos = data));
}
}
const todoStore = new TodoStore();
export default todoStore;
上面代码就是MobX 中的store 的写法。通过 class
定义我们的todo 应用状态,其中我们可以直接在 class
中直接定义状态以及更新状态的函数。最后我们 new
出一个todoStore 实例并export 出去,这样保证我们的react 应用中只会有一个todo 全局状态。
可以看到比起redux 要写一堆actions, reducers 等等,显然MobX 相对非常的方便,也好理解,而且还可以分很多个store。
唯一不一样的地方就是todoStore 这个 class
中的constructor
。这边我们引入mobx 提供的 makeAutoObservable
函数,makeAutoObservable(this)
的用意就是说当我们的应用启动,new 出todoStore 的唯一实例的同时,让todoStore 变得可被观察,也就是说可以追踪todoStore 所有state 的变化,并通知整个应用中所有引用了todoStore 的组件。
组件中MobX 生效
MobX 中上面就定义好了整个store 以及相应的actions 了,因为可以直接对state 做更改,所以也没有了reducer 的概念。接下来就是要在具体的组件中获取store。
这边直接以添加todo 的例子来看看怎么使用:
// /components/TodoAdd.tsx
import * as React from "react";
import { Button, Input, Grid } from "@chakra-ui/react";
import todoStore from "../store/todoStore";
import { observer } from "mobx-react";
function TodoAdd() {
return (
<Grid pt={2} templateColumns="5fr 1fr" columnGap="3">
<Input
placeholder="New todo"
value={todoStore.newTodo}
onChange={(e) => (todoStore.newTodo = e.target.value)}
/>
<Button onClick={() => todoStore.addTodo()}>Add Todo</Button>
</Grid>
);
}
export default observer(TodoAdd);
我们直接将export 的todoStore 引入,这样就可以直接使用todoStore 中的state 跟action。
不一样的地方在我们引入了mobx-react 提供的 observer
函数。这个 observer
函数的目的就是让该组件和所有经过 makeAutoObservable
的store(可能不只一个)配合起来,该组件中只要依赖了被observe 的state 发生了变化就会同步更新视图。
observer
函数会返回一个新的组件签名,默认就是原组件名,我们也可以改变名字。总之就是将原组件做了一层包装,使其和 makeAutoObservable
的store 绑定起来。
// /components/TodoList.tsx
import * as React from "react";
import { Button, Input, Flex, Checkbox, Heading } from "@chakra-ui/react";
import todoStore, { Todo } from "../store/todoStore";
import { observer } from "mobx-react";
function TodoListItems() {
return (
<>
{todoStore.todos.map((todo: Todo) => (
<Flex pt={2} key={todo.id}>
<Checkbox onClick={() => (todo.done = !todo.done)} />
<Input
mx={2}
value={todo.text}
onChange={(e) => (todo.text = e.target.value)}
/>
<Button onClick={() => todoStore.removeTodo(todo.id)}>Delete</Button>
</Flex>
))}
</>
);
}
const TodoListItemsObserver = observer(TodoListItems);
function TodoList() {
return (
<>
<Heading>Todo List</Heading>
<TodoListItemsObserver />
</>
);
}
export default TodoList;
// /components/TopBar.tsx
import * as React from "react";
import { Button, Grid } from "@chakra-ui/react";
import { ColorModeSwitcher } from "./ColorModeSwitcher";
import todoStore from '../store/todoStore';
function TopBar() {
const load = () => {
todoStore.loadTodo("https://raw.githubusercontent.com/jherr/todos-four-ways/master/data/todos.json")
}
return (
<Grid pt={2} templateColumns="1fr 1fr" columnGap="3">
<ColorModeSwitcher />
<Button onClick={load}>Load</Button>
</Grid>
);
}
export default TopBar;
MobX 装饰器
上面其实就是MobX 大概的用法,不过我个人觉得要把组件外面还要包一层 observer
有点丑,而且这么做把代码跟状态又有点硬编码在一起。
这边介绍另一种使用MobX 的方式,decorators。Decorator 的中文是装饰器,如果有用过springboot 的可能就知道,其实跟spring 中的注释(annotation)很像。在具体使用MobX decorators 之前有一些工作要准备。
MobX Decorators 准备工作
在react 中使用MobX 如果想配合着decorators 使用有一些配置工作,个人感觉过程有不少坑,在此纪录一下。
首先我们要安装两个额外的包:
- react-app-rewired
- react-app-rewire-mobx
// npm
npm install react-app-rewired react-app-rewire-mobx
// yarn
yarn add react-app-rewired react-app-rewire-mobx
其实我们的目的就是要重写cra 的配置,让cra 可以认识decorators,所以其实有另一个方法是 npm eject
弹出配置,不过因为这个过程是不可逆的,所以通常比较不推荐。
react-app-rewired
的功能就是协助我们重写cra 这个react 脚手架的配置。react-app-rewired
提供了一个injectBabelPlugin
,通过这个函数我们可以改变配置加入一些babel plugin。
根据官方文档,我们需要再/src 下面新建一个名为 config-overrides.js
的文件,并在该文件中我们就可以开始添加新的babel 插件更改配置等等,这边我们加上一个 babel-plugin-styled-components
作为示范(当然也要先装,npm/yarn),因为在代码中尝试使用了一点点styled-components。
要注意的是,在 config-overrides.js
中只能使用CommonJS 的语法,不支持ES6 import/export 的语法。
// config-overrides.js
const { injectBabelPlugin } = require("react-app-rewired");
const rewireMobX = require("react-app-rewire-mobx");
module.exports = function override(config, env) {
config = injectBabelPlugin("babel-plugin-styled-components", config);
config = rewireMobX(config, env);
return config;
};
具体代码如上,最后还有一个要注意的点。我们还要到package.json 中修改一下scripts 的配置,因为我们通过 react-app-rewired
重写了一些配置,为了让我们的配置在启动时被启用,我们就不能再使用default cra 为我们提供了react 脚本,而要使用 react-app-rewired
提供了,更改如下:
"scripts": {
- "start": "react-scripts start",
+ "start": "react-app-rewired start",
- "build": "react-scripts build",
+ "build": "react-app-rewired build",
- "test": "react-scripts test",
+ "test": "react-app-rewired test",
"eject": "react-scripts eject"
}
github 上源项目中特别提醒不要更改 eject
的脚本,原因就是因为当我们不再需要 react-app-rewired
后我们仍然可以通过执行default 的 eject
来重置配置,且 react-app-rewired
也没有提供对 eject
的脚本,所以不要乱来hhh。
使用 MobX Decorators
经过上面的配置我们就可以在react 中使用mobx 装饰器了,接下来就介绍怎么使用以及一些特别有用的装饰器。
下面会实现一个简易更改主题的效果,配合mobx 装饰器以及styled-components。
同样我们通过 class
来组织我们的store,这边我们设计一个theme 属性,通过theme 的变化要触发相应的视图渲染,也就是说theme 这个state 应该要是observable
,这边引出第一个decorator。
@observable
通过 @observable
就可以使我们的state 变成可观察的,也就是说state 的变化会引发通知任何依赖该state 的组件进行重新渲染。
import { observable } from 'mobx';
@observable
theme = "day";
@行动
@action
装饰器可以允许我们对 @observable
的state 进行更改。
@action
toggleTheme = () => {
this.theme = this.theme === "day" ? "night" : "day";
};
@计算
@computed
可以说是计算属性,这个装饰器可以允许我们基于observable 的state 返回关于state 的计算属性,其实跟vue 中的 computed
很像。
@observable
todos = [];
@action
addTodo = (todo) => {
this.todos.push(todo);
};
@computed
get todoCount() {
return this.todos.length;
};
Store 代码
// /stores/UiStore.js
import { action, observable } from "mobx";
class UiStore {
@observable
theme = "day";
@action
toggleTheme = () => {
this.theme = this.theme === "day" ? "night" : "day";
};
}
const uiStore = new UiStore();
export default uiStore;
// /stores/TodoStore.js
import { action, observable, computed } from "mobx";
class TodoStore {
@observable
todos = [];
@action
addTodo = (todo) => {
this.todos.push(todo);
};
@computed
get todoCount() {
return this.todos.length;
}
}
const todoStore = new TodoStore();
export default todoStore;
提供者
在MobX 中我们可能会有很多个store,而我们必须要使这些store 和应用装配起来。这边跟redux 很像,通过一个全局的 Provider
标签将所有需要装配的store 装配起来:
// /stores/Store.js
import uiStore from "./UiStore";
import todoStore from "./TodoStore";
class Store {
constructor() {
this.uiStore = uiStore;
this.todoStore = todoStore;
}
}
const store = new Store();
export default store;
// index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import registerServiceWorker from "./registerServiceWorker";
import { Provider } from "mobx-react";
import store from './stores/Store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
registerServiceWorker();
我们新建一个store 作为应用的根store,并且通过 Provider
与整个应用装配在一起。
@注入
上面我们做好了mobx 中的store 的设计,也跟应用装配在一起,现在就是要在组件中实际用上这些store。
@inject
装饰器加在类组件之上,告诉组件要注入哪些store。
@inject('store')
class App extends Component {
// ...
}
@观察者
@observer
装饰器其实跟上面不用装饰器的 observer()
作用一样,就是让组件中只要依赖了被observe 的state 发生了变化就会同步更新视图,与 @inject
的store 有关。
@inject('store')
@observer
class App extends Component {
render() {
const { uiStore, todoStore } = this.props.store;
return (
// code
)
}
}
MobX 异步
上面还算详细的介绍了mobx 的用法以及配合react 的方式。不过注意到,上面对store 数据的操作都是同步的,这边就来介绍一下在mobx 中的异步怎么做,其实也很简单的。
其实异步一样是action,最一开始基本的mobx 中其实有写到一个异步,如下:
@action
loadTodo(url: string) {
fetch(url)
.then((res) => res.json())
.then((data) => (this.todos = data));
}
感觉上好像没什么问题,但其实这是比较不好的写法。如果我们的react 应用启用了 use Strict
的检查,那就上面代码会出现以下报错:
Unhandled Rejection(Error): [mobx] Since strict-mode is enabled, changing observed observable values outside actions are not allowed.
1
原因就是因为,在mobx 中规定action 对observable state 的修改必须在action 体内,而上面代码显然把对todos 的更改放在了 then
回调函数内。
当然,第一种方式就是再多写一个action 专门用于改变state 的值,类似setter 概念,不过如果有很多state 那这种方式显然不好。
所以我们借住mobx 提供了一个 runInAction
函数,它接受一个函数作为参数,里面写我们的逻辑就好,很方便,如下:
@action
loadTodos = (url) => {
fetch(url)
.then((res) => res.json())
.then((data) => {
runInAction(() => {
this.todos = data;
});
});
};
或是你喜欢用 async/await
当然也可以:
@action
loadTodos = async url => {
const res = await fetch(url);
const data = await res.json();
runInAction(() => {
this.todos = data;
})
}
MobX 与 Redux
最后,就来说说mobx 跟redux 到底差别在什么地方,个人感觉在了解技术的同时也要知道到底有什么区别。
- Redux 原理
- MobX 原理
总结来说,mobx 和redux 有以下差异:
- 函数式和面向对象
- 单一store和多store
- JavaScript对象和可观察对象
- state 不可变(Immutable)和可变(Mutable)
这边分享一篇关于mobx 跟redux 的比较,还算详细,你需要Mobx还是Redux?。
结语
本篇介绍了MobX 这个react 状态管理工具,整体应该还算详细,使用上感觉确实比redux 直观一点,不过还是要在具体项目中实际实践才能更有感觉。希望看完这篇对不知道mobx 的你有一些帮助,若有错误或不完整也欢迎请多指教!
参考
参考 | 链接 |
---|---|
MobX - 文档 | https://doc.ebichu.cc/mobx/refguide/object.html |
react-app-rewired | https://github.com/timarney/react-app-rewired#how-to-rewire-your-create-react-app-project |
重新连接 create-react-app 以使用 MobX | https://www.npmjs.com/package/react-app-rewire-mobx/v/1.0.8 |
react-app-rewired使用 | http://wmm66.com/index/article/detail/id/165.html |
create-react-app + mobx + yarn 架子,你要的轮子 | https://www.jianshu.com/p/181be41fba6b |
终于讲清楚了nodejs中exports和module.exports的区别 | https://blog.csdn.net/qq_31967569/article/details/82461499 |
你需要Mobx还是Redux? | https://juejin.cn/post/6844903562095362056 |