MobX6
1. MobX 概述
MobX 是一个简单的可扩展的状态管理库,无样板代码风格简约。
目前最新版本为 6,版本 4 和版本 5 已不再支持。
在 MobX 6 中不推荐使用装饰器语法,因为它不是 ES 标准,并且标准化过程要花费很长时间,但是通过配置仍然可以启用装饰器语法。
MobX 可以运行在任何支持 ES5 的环境中,包含浏览器和 Node。
MobX 通常和 React 配合使用,但是在 Angular 和 Vue 中也可以使用 MobX。
2. MobX 光速入门
2.1 下载
- mobx:MobX 核心库
- mobx-react-lite:仅支持函数组件
- mobx-react:既支持函数组件也支持类组件
yarn add mobx@6.1.8 mobx-react-lite@3.2.0
2.2 核心概念
- observable state:被 MobX 跟踪的状态。
- action:允许修改状态的方法,在严格模式下只有 action 方法被允许修改状态。
- computed:根据应用程序状态派生的新值,计算值。
2.3 工作流程
2.4 入门案例
2.4.1 案例
计数器:在组件中显示数值状态,单击按钮使数值加一,单击按钮使数值重置。
Counter.js 组件
import { observer } from "mobx-react-lite";
function Counter({ counterStore }) {
return (
<div>
<p className="paragraph">{counterStore.count}</p>
<button onClick={() => counterStore.increment()} className="button">
加 1
</button>
<button onClick={() => counterStore.reset()} className="button">
重置
</button>
</div>
);
}
// observer: 监控当前组件使用到的由 MobX 跟踪的 observable state, 当状态发生变化时通知 React 更新视图
export default observer(Counter);
CountStore.js
import { makeAutoObservable } from "mobx";
class CounterStore {
// 数值状态
count = 10;
constructor() {
// 将参数对象中的属性设置为 observable state
// 将参数对象中的方法设置为 action
makeAutoObservable(this);
}
// 使数值状态加一
increment() {
this.count += 1;
}
// 重置数值状态
reset() {
this.count = 0;
}
}
export default CounterStore;
App.js
// 导入 Counter 组件
import Counter from "./components/Counter/Counter";
// 导入管理 Counter 组件的 Store
import CounterStore from "./stores/Counter/CounterStore";
// 创建管理 Counter 组件的 Store 实例对象
const counterStore = new CounterStore();
function App() {
// 调用 Counter 组件并传入管理其状态的 Store
return <Counter counterStore={counterStore} />;
}
export default App;
2.4.2 makeAutoObservable
// target: 将目标对象中的属性和方法设置为 observable state 和 action
// overrides: 覆盖默认设置, 将 target 对象中的某些属性或者方法设置为普通属性
// options: 配置对象, autoBind, 使 action 方法始终拥有正确的 this 指向
makeAutoObservable(target, overrides?, options?)
// 示例
makeAutoObservable(this, {reset: false}, {autoBind: true})
2.4.3 总结
状态变化更新视图的必要条件
- 状态需要被标记为
observable state
- 更改状态的方法需要被标记为
action
方法 - 组件视图必须通过
observer
方法包裹
可以使用 makeAutoObservable
方法将对象属性设置为 observable state
,将对象方法设置为 action
方法
可以使用 observer
方法监控当前组件使用到的由 MobX 跟踪的 observable state
,当状态发生变化时通知 React 更新视图
3. TodoList
3.1 创建初始 TodoListStore
-
创建 TodoListStore,用于管理待办事项列表状态。
// TodoListStore.js class TodoListStore { todos = []; constructor(todos) { if (todos) this.todos = todos; } } export default TodoListStore;
-
创建 TodoViewStore,用于管理待办事项状态。
// TodoViewStore.js class TodoViewStore { id = Math.random(); title = ""; completed = false; constructor(title) { this.title = title; } } export default TodoViewStore;
-
创建 TodoListStore 实例对象并将其传入到 TodoListView 组件
// App.js import TodoListView from "./components/Todos/TodoListView"; import TodoListStore from "./stores/Todos/TodoListStore"; import TodoStore from "./stores/Todos/TodoStore"; // const counterStore = new CounterStore(); const todoListStore = new TodoListStore([ new TodoStore("Hello MobX"), new TodoStore("Hello React"), ]); function App() { return <TodoListView TodoListStore={todoListStore} />; }
-
渲染初始待办事项列表
// TodoListView.js function TodoListView({ TodoListStore }) { return ( <ul className="todo-list"> {TodoListStore.todos.map((todo) => ( <TodoView key={todo.id} todo={todo} /> ))} </ul> ); }
// TodoView.js function TodoView({ todo }) { return <label>{todo.title}</label>; }
3.2 创建待办事项
-
在 TodoListStore 类中创建 createTodo 方法,用于向 todos 数组中添加待办事项
// TodoListStore.js import TodoStore from "./TodoStore"; class TodoListStore { createTodo(title) { this.todos.push(new TodoStore(title)); } }
-
在 TodoHeader 组件中调用 createTodo 方法创建待办事项
// TodoListView.js function TodoListView({ TodoListStore }) { return <TodoHeader createTodo={(title) => TodoListStore.createTodo(title)} />; }
// TodoHeader.js import { useState } from "react"; function TodoHeader({ createTodo }) { const [title, setTitle] = useState(""); return ( <header className="header"> <input value={title} onChange={(event) => setTitle(event.target.value)} onKeyUp={(event) => { if (event.key === "Enter") { createTodo(title); setTitle(""); } }} /> </header> ); }
-
设置状态变化更新视图的必要条件
// TodoListStore.js import { action, makeObservable, observable } from "mobx"; class TodoListStore { constructor() { makeObservable(this, { todos: observable, createTodo: action, }); } }
// TodoListView.js import { observer } from "mobx-react-lite"; function TodoListView() {} export default observer(TodoListView);
3.3 创建 Store 上下文
我们希望在每个组件中都能够直接获取到 TodoListStore,而不是通过 props 属性传递的方式。
// TodoListStore.js
import TodoStore from "./TodoStore";
import { createContext, useContext } from "react";
class TodoListStore {
createTodo(title) {
this.todos.push(new TodoStore(title));
}
}
const TodoListStoreContext = createContext();
const TodoListStoreProvider = ({ store, children }) => {
return <TodoListStoreContext.Provider value={store}>{children}</TodoListStoreContext.Provider>;
};
const useTodoListStore = () => {
return useContext(TodoListStoreContext);
};
export { TodoListStore, TodoListStoreProvider, useTodoListStore };
// App.js
import { TodoListStore, TodoListStoreProvider } from "./stores/Todos/TodoListStore";
function App() {
return (
<TodoListStoreProvider store={todoListStore}>
<TodoListView />
<Counter counterStore={counterStore} />
</TodoListStoreProvider>
);
}
// TodoListView.js
import { useTodoListStore } from "../../stores/Todos/TodoListStore";
function TodoListView() {
const todoListStore = useTodoListStore();
}
import { useTodoListStore } from "../../stores/Todos/TodoListStore";
function TodoHeader() {
const todoListStore = useTodoListStore();
todoListStore.createTodo(title);
}
3.4 删除待办事项
// TodoListStore.js
class TodoListStore {
constructor() {
makeObservable(this, {
removeTodo: action,
});
}
removeTodo(id) {
const index = this.todos.findIndex((todo) => todo.id === id);
this.todos.splice(index, 1);
}
}
// TodoView.js
import { useTodoListStore } from "../../stores/Todos/TodoListStore";
function TodoView({ todo }) {
const todoListStore = useTodoListStore();
return <button onClick={() => todoListStore.removeTodo(todo.id)} className="destroy" />;
}
3.5 更改任务状态
// TodoStore.js
import { makeObservable, observable, action } from "mobx";
class TodoStore {
completed = false;
constructor() {
makeObservable(this, {
completed: observable,
toggle: action,
});
}
toggle() {
this.completed = !this.completed;
}
}
// TodoView.js
import { observer } from "mobx-react-lite";
function TodoView({ todo }) {
return (
<li className={todo.completed ? "completed" : ""}>
<input
checked={todo.completed}
onChange={() => todo.toggle()}
className="toggle"
type="checkbox"
/>
</li>
);
}
export default observer(TodoView);
3.6 更正 this 指向
<input onChange={() => todo.toggle()} />
<input onChange={todo.toggle}/> // this 指向会发生错误
makeObservable(this, {
toggle: action.bound,
});
3.7 计算待办事项数量
待办事项数量属于派生状态,即该状态依赖现有状态 (todos) 生成。
派生状态可以使用计算值实现,当依赖状态发生变化后,计算值自动更新。
// TodoListStore.js
import { computed } from "mobx";
class TodoListStore {
constructor() {
makeObservable(this, {
unCompletedTodoCount: computed,
});
}
get unCompletedTodoCount() {
return this.todos.filter((todo) => !todo.completed).length;
}
}
// TodoFooter.js
import { useTodoListStore } from "../../stores/Todos/TodoListStore"
import { observer } from "mobx-react-lite"
function TodoFooter() {
const todoListStore = useTodoListStore()
return <strong>{todoListStore.unCompletedTodoCount}</strong> item left
}
export default observer(TodoFooter)
注意:计算值是被缓存的。
get unCompletedTodoCount() {
console.log("unCompletedTodoCount")
return this.todos.filter(todo => !todo.completed).length
}
{
todoListStore.unCompletedTodoCount;
}
{
todoListStore.unCompletedTodoCount;
}
{
todoListStore.unCompletedTodoCount;
}
// 计算属性被调用多次, 但是方法内部的console.log 只会输出一次, 说明计算属性是被缓存的.
3.8 待办事项过滤
待办事项过滤为计算值的练习任务。
// TodoListStore.js
class TodoListStore {
todos = [];
filter = "all";
constructor() {
makeObservable(this, {
filter: observable,
changeFilter: action,
filterTodos: computed,
});
}
get filterTodos() {
switch (this.filter) {
case "all":
return this.todos;
case "active":
return this.todos.filter((todo) => !todo.completed);
case "completed":
return this.todos.filter((todo) => todo.completed);
default:
return this.todos;
}
}
changeFilter(filter) {
this.filter = filter;
}
}
// TodoFooter.js
function TodoFooter() {
const todoListStore = useTodoListStore();
return (
<footer className="footer">
<ul className="filters">
<li>
<button
onClick={() => todoListStore.changeFilter("all")}
className={todoListStore.filter === "all" ? "selected" : ""}
>
All
</button>
</li>
<li>
<button
onClick={() => todoListStore.changeFilter("active")}
className={todoListStore.filter === "active" ? "selected" : ""}
>
Active
</button>
</li>
<li>
<button
onClick={() => todoListStore.changeFilter("completed")}
className={todoListStore.filter === "completed" ? "selected" : ""}
>
Completed
</button>
</li>
</ul>
</footer>
);
}
// TodoListView.js
function TodoListView() {
const todoListStore = useTodoListStore();
return (
<ul className="todo-list">
{todoListStore.filterTodos.map((todo) => (
<TodoView key={todo.id} todo={todo} />
))}
</ul>
);
}
3.9 清除已办事务
// TodoListStore
class TodoListStore {
clearCompleted() {
this.todos = this.todos.filter((todo) => !todo.completed);
}
}
// TodoFooter.js
<button className="clear-completed" onClick={() => todoListStore.clearCompleted()}>
Clear completed
</button>
3.10 加载远端任务
-
下载并启动 json-server
npm install -g json-server
json-server ./src/todo.json --port 3005
yarn add axios
-
创建 loadTodos 方法用于加载初始任务
// TodoListStore.js import axios from "axios"; import { runInAction } from "mobx"; class TodoListStore { constructor(todos) { this.loadTodos(); } // 异步方法不能作为action方法 async loadTodos() { let todos = await axios.get("http://localhost:3005/todos").then((response) => response.data); runInAction(() => todos.forEach((todo) => this.todos.push(todo))); } }
-
解决远端加载待办事项无法切换状态的问题
原因是远端加载待办事项的任务对象的原型对象上没有 toggle 方法。
import TodoStore from "./TodoStore"; class TodoListStore { async loadTodos() { runInAction(() => todos.forEach((todo) => this.todos.push(new TodoStore(todo.title)))); } }
3.11 创建 RootStore
通过创建 RootStore 可以将 CounterStore 和 TodoListStore 进行合并,实现在任何组件中都可以访问任何状态,方便全局状态共享。
// RootStore.js
import CounterStore from "./Counter/CounterStore";
import TodoListStore from "./Todos/TodoListStore";
import { createContext, useContext } from "react";
class RootStore {
constructor() {
this.counterStore = new CounterStore();
this.todoListStore = new TodoListStore();
}
}
const RootStoreContext = createContext();
const RootStoreProvider = ({ store, children }) => {
return <RootStoreContext.Provider value={store}>{children}</RootStoreContext.Provider>;
};
const useRootStore = () => {
return useContext(RootStoreContext);
};
export { RootStore, RootStoreProvider, useRootStore };
// App.js
import Counter from "./components/Counter/Counter";
import TodoListView from "./components/Todos/TodoListView";
import { RootStore, RootStoreProvider } from "./stores/RootStore";
const rootStore = new RootStore();
function App() {
return (
<RootStoreProvider store={rootStore}>
<TodoListView />
<Counter />
</RootStoreProvider>
);
}
export default App;
// 消费 RootStore 的其他组件
import { useRootStore } from "../../stores/RootStore";
function TodoHeader() {
const { todoListStore } = useRootStore();
}
function Counter() {
const { counterStore } = useRootStore();
}
4. 数据监测
4.1 autorun 方法
-
监控数据变化执行副作用,接收一个函数作为参数,参数函数用来执行副作用,当参数函数内部使用的 observable state, computed 发生变化时函数会运行,初始运行 autorun 方法时参数函数也会运行一次。
import { autorun } from "mobx"; import { useEffect } from "react"; function Counter() { const { counterStore } = useRootStore(); useEffect(() => { // 确保 autorun 方法只被初始化一次 autorun(() => { console.log(counterStore.count); }); }, []); }
-
对于基本数据类型,属于值传递,mobx 只能跟踪到原始属性,跟踪不到复制后的值。
useEffect(() => { let count = counterStore.count; autorun(() => { // 错误写法, mobx 跟踪不到变量 count console.log(count); }); }, []);
-
对于引用数据类型,只要引用地址不发生变化,mobx 就可以进行跟踪
// CounterStore.js class CounterStore { person = { name: "张三" }; }
// Counter 组件 function Counter() { const { counterStore } = useRootStore(); useEffect(() => { const person = counterStore.person; autorun(() => { console.log(person.name); }); }, []); return ( <div> <p>{counterStore.person.name}</p> <button onClick={() => runInAction(() => (counterStore.person.name = "李四"))}> 李四 </button> <button onClick={() => runInAction(() => (counterStore.person = { name: "王五" }))}> 王五 </button> </div> ); }
-
去除控制台中和 useEffect 相关的 ⚠️
在 react 17 版本中,官方团队修改了脚手架工具,允许直接在外部声明 .eslintrc 文件覆盖 eslint 配置,不需要使用 package.json, react-app-rewired 和 customize-cra 就可用实现 eslint 配置
在项目的根目录下新建
.eslintrc.js
文件并加入如下内容module.exports = { plugins: ["react-hooks"], rules: { "react-hooks/exhaustive-deps": 0, }, };
注意此处不需要下载 eslint-plugin-react-hooks 插件,
create-react-app
脚手架工具内部已经内置。配置完成后重新启动应用。
4.2 reaction 方法
监控状态变化执行副作用,接收两个函数作为参数,第一个函数返回要监控的状态,第二个函数用来执行副作用,只有当第一个函数返回的状态发生变化时,第二个函数才会执行。reaction 方法提供了更加细颗粒度的状态控制。
和 autorun 不同,reaction 初始时不会执行副作用。
import { reaction } from "mobx";
function Counter() {
useEffect(() => {
reaction(
() => counterStore.count,
(current, previous) => {
console.log(current);
console.log(previous);
}
);
}, []);
}