# 前言
之前在学习 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 |