Mobx
MobX是一个基于信号的、经过实战测试的库,通过透明地应用函数式响应式编程,使状态管理变得简单和可扩展。

查看更多相关内容
什么是 MobX,它的工作原理是什么?MobX 是一个基于信号的、经过实战测试的状态管理库,通过透明地应用函数式响应式编程(FRP)使状态管理变得简单和可扩展。它通过观察者模式自动追踪状态变化,当可观察状态发生变化时,MobX 会自动更新所有依赖该状态的派生值和反应。
## 核心概念
### 1. Observable(可观察状态)
使用 `observable` 或 `makeObservable` 将普通 JavaScript 对象、数组、Map 等转换为可观察对象。可观察状态的变化会被 MobX 自动追踪。
```javascript
import { observable } from 'mobx';
class TodoStore {
@observable todos = [];
@observable filter = 'all';
}
```
### 2. Action(动作)
动作是修改状态的唯一方式,使用 `action` 装饰器或 `runInAction` 函数包装状态修改逻辑。这确保了状态变化是可追踪和可预测的。
```javascript
import { action } from 'mobx';
class TodoStore {
@action addTodo(text) {
this.todos.push({ text, completed: false });
}
}
```
### 3. Computed(计算值)
计算值是基于可观察状态自动更新的派生值,类似于 Vue 的 computed 属性。只有当依赖的可观察状态发生变化时才会重新计算。
```javascript
import { computed } from 'mobx';
class TodoStore {
@computed get activeTodos() {
return this.todos.filter(todo => !todo.completed);
}
}
```
### 4. Reaction(反应)
反应是当可观察状态变化时自动执行的副作用,类似于 React 的 useEffect。常用的反应类型包括 `autorun`、`reaction` 和 `when`。
```javascript
import { autorun } from 'mobx';
autorun(() => {
console.log('当前待办事项数量:', this.todos.length);
});
```
## 工作原理
MobX 的工作流程:
1. **追踪阶段**:当反应或计算值读取可观察状态时,MobX 会建立依赖关系
2. **变化阶段**:通过 action 修改可观察状态
3. **通知阶段**:MobX 自动通知所有依赖该状态的反应和计算值
4. **更新阶段**:反应和计算值自动重新执行或重新计算
## 与 Redux 的区别
| 特性 | MobX | Redux |
|------|------|-------|
| 状态管理 | 自动追踪,无需手动订阅 | 需要手动订阅和 dispatch |
| 代码量 | 较少,更简洁 | 较多,需要定义 actions、reducers |
| 学习曲线 | 较平缓 | 较陡峭 |
| 状态结构 | 可以嵌套 | 推荐扁平化 |
| 调试工具 | MobX DevTools | Redux DevTools |
## 最佳实践
1. **始终使用 action 修改状态**:确保状态变化可追踪
2. **合理使用 computed**:避免重复计算,提高性能
3. **避免过度使用 observable**:只对需要追踪的状态使用
4. **使用 makeAutoObservable**:简化装饰器配置
5. **分离业务逻辑和 UI**:将状态管理逻辑集中在 store 中
## 适用场景
MobX 适用于:
- 中大型 React 应用
- 需要复杂状态管理的项目
- 团队希望快速开发的项目
- 状态结构复杂且嵌套的场景
不适用于:
- 非常简单的应用
- 需要严格时间旅行调试的场景
- 团队偏好函数式编程范式
前端 · 2月22日 14:08
MobX 和 Redux 的区别是什么,如何选择?MobX 和 Redux 都是流行的状态管理库,但它们的设计理念和使用方式有很大的不同。选择哪一个取决于项目需求、团队偏好和具体场景。
## 核心设计理念
### MobX
- **基于观察者模式**:自动追踪状态变化,无需手动订阅
- **命令式编程**:直接修改状态,更符合直觉
- **透明响应式**:状态变化自动触发更新
- **灵活性高**:不强制特定的代码结构
### Redux
- **基于函数式编程**:使用纯函数处理状态变化
- **声明式编程**:通过 dispatch action 来修改状态
- **单向数据流**:Action → Reducer → Store → View
- **规范性高**:强制特定的代码结构
## 代码对比
### MobX 示例
```javascript
import { observable, action, computed, makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeAutoObservable(this);
}
@computed get filteredTodos() {
switch (this.filter) {
case 'completed':
return this.todos.filter(todo => todo.completed);
case 'active':
return this.todos.filter(todo => !todo.completed);
default:
return this.todos;
}
}
@action addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
}
@action toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
@action setFilter(filter) {
this.filter = filter;
}
}
const store = new TodoStore();
// 使用
store.addTodo('Learn MobX');
store.toggleTodo(store.todos[0].id);
```
### Redux 示例
```javascript
import { createStore } from 'redux';
// Action Types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const SET_FILTER = 'SET_FILTER';
// Action Creators
const addTodo = (text) => ({ type: ADD_TODO, payload: { id: Date.now(), text, completed: false } });
const toggleTodo = (id) => ({ type: TOGGLE_TODO, payload: id });
const setFilter = (filter) => ({ type: SET_FILTER, payload: filter });
// Reducer
const initialState = {
todos: [],
filter: 'all'
};
function todoReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return { ...state, todos: [...state.todos, action.payload] };
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
)
};
case SET_FILTER:
return { ...state, filter: action.payload };
default:
return state;
}
}
const store = createStore(todoReducer);
// Selector
const selectFilteredTodos = (state) => {
switch (state.filter) {
case 'completed':
return state.todos.filter(todo => todo.completed);
case 'active':
return state.todos.filter(todo => !todo.completed);
default:
return state.todos;
}
};
// 使用
store.dispatch(addTodo('Learn Redux'));
store.dispatch(toggleTodo(store.getState().todos[0].id));
```
## 详细对比
| 特性 | MobX | Redux |
|------|------|-------|
| **编程范式** | 命令式、面向对象 | 函数式、声明式 |
| **状态追踪** | 自动追踪 | 手动订阅 |
| **状态修改** | 直接修改 | 通过 action |
| **代码量** | 较少 | 较多 |
| **学习曲线** | 平缓 | 陡峭 |
| **灵活性** | 高 | 低 |
| **规范性** | 低 | 高 |
| **调试工具** | MobX DevTools | Redux DevTools |
| **时间旅行** | 有限支持 | 完整支持 |
| **中间件** | 不需要 | 丰富(redux-thunk、redux-saga 等) |
| **性能** | 自动优化 | 需要手动优化 |
| **状态结构** | 可以嵌套 | 推荐扁平化 |
| **类型支持** | 良好 | 需要额外配置 |
## 使用场景
### 适合使用 MobX 的场景
1. **快速开发**:需要快速原型开发或小型项目
2. **复杂状态结构**:状态结构复杂且嵌套
3. **团队经验**:团队更熟悉面向对象编程
4. **灵活性优先**:需要更多的代码灵活性
5. **学习成本**:希望降低学习成本
```javascript
// MobX 适合复杂嵌套状态
class UserStore {
@observable user = {
profile: {
name: '',
email: '',
address: {
city: '',
country: ''
}
},
preferences: {
theme: 'light',
language: 'en'
}
};
@action updateCity(city) {
this.user.profile.address.city = city; // 直接修改嵌套属性
}
}
```
### 适合使用 Redux 的场景
1. **大型项目**:需要严格规范的大型项目
2. **团队协作**:多人协作,需要统一的代码规范
3. **时间旅行**:需要完整的时间旅行调试功能
4. **中间件需求**:需要使用丰富的中间件生态
5. **函数式编程**:团队偏好函数式编程范式
```javascript
// Redux 适合扁平化状态
const initialState = {
users: {
byId: {},
allIds: []
},
profiles: {
byId: {},
allIds: []
},
addresses: {
byId: {},
allIds: []
}
};
// 通过 reducer 处理状态变化
function reducer(state = initialState, action) {
switch (action.type) {
case UPDATE_CITY:
return {
...state,
addresses: {
...state.addresses,
byId: {
...state.addresses.byId,
[action.payload.id]: {
...state.addresses.byId[action.payload.id],
city: action.payload.city
}
}
}
};
default:
return state;
}
}
```
## 性能对比
### MobX 性能优势
1. **自动优化**:自动追踪依赖,只更新必要的组件
2. **批量更新**:action 内部的状态变化会被批量处理
3. **懒计算**:computed 值只在被访问时才计算
```javascript
// MobX 自动优化
class Store {
@observable items = [];
@computed get expensiveValue() {
console.log('Computing expensive value');
return this.items.reduce((sum, item) => sum + item.value, 0);
}
}
// 只有在访问 expensiveValue 时才会计算
console.log(store.expensiveValue); // 计算一次
console.log(store.expensiveValue); // 使用缓存,不计算
```
### Redux 性能挑战
1. **手动优化**:需要使用 reselect、memo 等工具优化
2. **全量比较**:每次 dispatch 都会比较整个状态树
3. **需要手动订阅**:需要手动选择需要的数据
```javascript
// Redux 需要手动优化
import { createSelector } from 'reselect';
const selectItems = (state) => state.items;
const selectExpensiveValue = createSelector(
[selectItems],
(items) => {
console.log('Computing expensive value');
return items.reduce((sum, item) => sum + item.value, 0);
}
);
// 需要手动选择数据
const value = selectExpensiveValue(store.getState());
```
## 调试对比
### MobX 调试
```javascript
// 使用 MobX DevTools
import { makeObservable, observable, action } from 'mobx';
class Store {
@observable count = 0;
constructor() {
makeObservable(this);
}
@action increment() {
this.count++;
}
}
// 在浏览器中查看状态变化
```
### Redux 调试
```javascript
// 使用 Redux DevTools
import { createStore } from 'redux';
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
// 可以查看完整的 action 历史、状态变化和时间旅行
```
## 迁移建议
### 从 MobX 迁移到 Redux
1. **重构状态结构**:将嵌套状态扁平化
2. **创建 action types**:定义所有可能的 action
3. **编写 reducers**:将状态修改逻辑移到 reducers
4. **使用中间件**:根据需要添加中间件
5. **更新组件**:使用 useSelector 和 useDispatch
### 从 Redux 迁移到 MobX
1. **创建 stores**:将 reducer 逻辑转换为 stores
2. **使用 observable**:将状态转换为 observable
3. **添加 actions**:将 action creators 转换为 actions
4. **更新组件**:使用 observer 或 useObserver
5. **简化代码**:移除不必要的样板代码
## 总结
**选择 MobX 如果:**
- 需要快速开发
- 状态结构复杂且嵌套
- 团队更熟悉面向对象编程
- 希望降低学习成本
- 需要更多的代码灵活性
**选择 Redux 如果:**
- 项目规模较大
- 需要严格的代码规范
- 需要完整的时间旅行调试
- 需要丰富的中间件生态
- 团队偏好函数式编程
两者都是优秀的状态管理库,选择哪一个应该基于项目需求和团队情况。在实际项目中,也可以根据不同模块的特点混合使用。
前端 · 2月22日 14:08
MobX 6 相比 MobX 4/5 有哪些重要变化?MobX 6 是 MobX 的最新主要版本,相比 MobX 4/5 有许多重要的变化和改进。了解这些变化对于升级和维护项目非常重要。
## 主要变化
### 1. 移除装饰器支持
MobX 6 默认不再支持装饰器语法,推荐使用 `makeObservable` 或 `makeAutoObservable`。
**MobX 4/5(装饰器语法):**
```javascript
import { observable, action, computed } from 'mobx';
class TodoStore {
@observable todos = [];
@observable filter = 'all';
@computed get completedTodos() {
return this.todos.filter(todo => todo.completed);
}
@action addTodo(text) {
this.todos.push({ text, completed: false });
}
}
```
**MobX 6(推荐方式):**
```javascript
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeAutoObservable(this);
}
get completedTodos() {
return this.todos.filter(todo => todo.completed);
}
addTodo(text) {
this.todos.push({ text, completed: false });
}
}
```
### 2. makeObservable 和 makeAutoObservable
MobX 6 引入了两个新的 API 来替代装饰器:
#### makeObservable
需要显式指定每个属性的类型。
```javascript
import { makeObservable, observable, action, computed } from 'mobx';
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeObservable(this, {
todos: observable,
filter: observable,
completedTodos: computed,
addTodo: action
});
}
get completedTodos() {
return this.todos.filter(todo => todo.completed);
}
addTodo(text) {
this.todos.push({ text, completed: false });
}
}
```
#### makeAutoObservable(推荐)
自动推断属性类型,更简洁。
```javascript
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeAutoObservable(this);
}
get completedTodos() {
return this.todos.filter(todo => todo.completed);
}
addTodo(text) {
this.todos.push({ text, completed: false });
}
}
```
### 3. 移除 configure
MobX 6 移除了 `configure` API,不再需要全局配置。
**MobX 4/5:**
```javascript
import { configure } from 'mobx';
configure({
enforceActions: 'always',
useProxies: 'ifavailable'
});
```
**MobX 6:**
```javascript
// 不再需要 configure,默认行为已经优化
```
### 4. 移除 extras
MobX 6 移除了 `extras` API,相关功能被整合到主 API 中。
**MobX 4/5:**
```javascript
import { extras } from 'mobx';
const isObservable = extras.isObservable(obj);
```
**MobX 6:**
```javascript
import { isObservable } from 'mobx';
const isObservable = isObservable(obj);
```
### 5. 移除 intercept 和 observe
MobX 6 移除了 `intercept` 和 `observe` API,推荐使用 `reaction` 替代。
**MobX 4/5:**
```javascript
import { observe } from 'mobx';
const disposer = observe(store.todos, (change) => {
console.log('Todo changed:', change);
});
```
**MobX 6:**
```javascript
import { reaction } from 'mobx';
const disposer = reaction(
() => store.todos.length,
(length) => {
console.log('Todo count changed:', length);
}
);
```
### 6. 类型推断改进
MobX 6 对 TypeScript 的支持更好,类型推断更准确。
```typescript
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos: Todo[] = [];
filter: 'all' | 'completed' | 'active' = 'all';
constructor() {
makeAutoObservable<TodoStore>(this);
}
get completedTodos(): Todo[] {
return this.todos.filter(todo => todo.completed);
}
addTodo(text: string): void {
this.todos.push({ text, completed: false });
}
}
```
### 7. 移除 mobx-react 的 inject 和 Provider
MobX 6 推荐使用 React Context API,不再需要 `inject` 和 `Provider`。
**MobX 4/5:**
```javascript
import { Provider, inject, observer } from 'mobx-react';
@inject('todoStore')
@observer
class TodoList extends React.Component {
render() {
const { todoStore } = this.props;
return <div>{/* ... */}</div>;
}
}
function App() {
return (
<Provider todoStore={todoStore}>
<TodoList />
</Provider>
);
}
```
**MobX 6:**
```javascript
import { observer } from 'mobx-react-lite';
import { createContext, useContext } from 'react';
const TodoContext = createContext(null);
function TodoProvider({ children, store }) {
return (
<TodoContext.Provider value={store}>
{children}
</TodoContext.Provider>
);
}
function useTodoStore() {
const store = useContext(TodoContext);
if (!store) {
throw new Error('useTodoStore must be used within TodoProvider');
}
return store;
}
const TodoList = observer(() => {
const store = useTodoStore();
return <div>{/* ... */}</div>;
});
function App() {
return (
<TodoProvider store={todoStore}>
<TodoList />
</TodoProvider>
);
}
```
### 8. 性能改进
MobX 6 在性能方面有许多改进:
1. **更小的包体积**:通过 Tree-shaking 减少包体积
2. **更快的响应速度**:优化了依赖追踪算法
3. **更好的内存管理**:减少了内存占用
### 9. 错误处理改进
MobX 6 提供了更清晰的错误信息。
```javascript
// MobX 6 会提供更清晰的错误信息
class Store {
data = [];
constructor() {
makeAutoObservable(this);
}
// 如果在 action 外修改状态
modifyData() {
this.data.push({}); // 警告:在 action 外修改状态
}
}
```
## 迁移指南
### 从 MobX 4/5 迁移到 MobX 6
#### 1. 移除装饰器
**之前:**
```javascript
class Store {
@observable data = [];
@action
addData(item) {
this.data.push(item);
}
}
```
**之后:**
```javascript
class Store {
data = [];
constructor() {
makeAutoObservable(this);
}
addData(item) {
this.data.push(item);
}
}
```
#### 2. 更新 mobx-react
**之前:**
```javascript
import { Provider, inject, observer } from 'mobx-react';
@inject('store')
@observer
class Component extends React.Component {
render() {
const { store } = this.props;
return <div>{store.data}</div>;
}
}
```
**之后:**
```javascript
import { observer } from 'mobx-react-lite';
const Component = observer(() => {
const store = useStore();
return <div>{store.data}</div>;
});
```
#### 3. 移除 configure
**之前:**
```javascript
import { configure } from 'mobx';
configure({
enforceActions: 'always'
});
```
**之后:**
```javascript
// 不再需要 configure
```
#### 4. 更新 extras
**之前:**
```javascript
import { extras } from 'mobx';
if (extras.isObservable(obj)) {
// ...
}
```
**之后:**
```javascript
import { isObservable } from 'mobx';
if (isObservable(obj)) {
// ...
}
```
## 最佳实践
### 1. 使用 makeAutoObservable
```javascript
class Store {
data = [];
constructor() {
makeAutoObservable(this);
}
}
```
### 2. 使用 mobx-react-lite
```javascript
import { observer } from 'mobx-react-lite';
const Component = observer(() => {
return <div>{/* ... */}</div>;
});
```
### 3. 使用 React Context
```javascript
const StoreContext = createContext(null);
function StoreProvider({ children, store }) {
return (
<StoreContext.Provider value={store}>
{children}
</StoreContext.Provider>
);
}
function useStore() {
const store = useContext(StoreContext);
if (!store) {
throw new Error('useStore must be used within StoreProvider');
}
return store;
}
```
### 4. 使用 TypeScript
```typescript
class Store {
data: Data[] = [];
constructor() {
makeAutoObservable<Store>(this);
}
}
```
## 常见问题
### 1. 如何继续使用装饰器?
如果需要继续使用装饰器,可以安装 `mobx-undecorate` 包。
```javascript
import { decorate, observable, action } from 'mobx';
class Store {
data = [];
addData(item) {
this.data.push(item);
}
}
decorate(Store, {
data: observable,
addData: action
});
```
### 2. 如何处理类型推断?
使用 `makeAutoObservable` 时,可以传入泛型参数。
```typescript
class Store {
data: Data[] = [];
constructor() {
makeAutoObservable<Store>(this);
}
}
```
### 3. 如何处理 action.bound?
使用 `action.bound` 时,需要在 `makeObservable` 中指定。
```javascript
class Store {
data = [];
constructor() {
makeObservable(this, {
data: observable,
addData: action.bound
});
}
addData(item) {
this.data.push(item);
}
}
```
## 总结
- MobX 6 移除了装饰器支持,推荐使用 `makeAutoObservable`
- 移除了 `configure`、`extras`、`intercept`、`observe` 等 API
- 推荐使用 `mobx-react-lite` 和 React Context
- 对 TypeScript 的支持更好
- 性能和错误处理都有改进
- 迁移相对简单,主要是替换装饰器语法
前端 · 2月22日 14:08
MobX 中 action 的作用和使用方法是什么?在 MobX 中,action 是修改 observable 状态的唯一推荐方式。使用 action 可以确保状态变化是可追踪、可预测的,并且能够优化性能。
## Action 的作用
1. **批量更新**:action 内部的所有状态变化会被批量处理,减少不必要的重新计算
2. **可追踪性**:所有状态变化都集中在 action 中,便于调试和追踪
3. **事务性**:action 内部的状态变化是原子的,要么全部成功,要么全部失败
4. **性能优化**:减少 reaction 的触发次数,提高性能
## Action 的使用方式
### 1. 使用装饰器
```javascript
import { observable, action } from 'mobx';
class TodoStore {
@observable todos = [];
@action addTodo(text) {
this.todos.push({ text, completed: false });
}
@action.bound removeTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id);
}
@action async fetchTodos() {
const response = await fetch('/api/todos');
const data = await response.json();
this.todos = data;
}
}
```
### 2. 使用 makeObservable
```javascript
import { makeObservable, observable, action } from 'mobx';
class TodoStore {
todos = [];
constructor() {
makeObservable(this, {
todos: observable,
addTodo: action,
removeTodo: action.bound,
fetchTodos: action
});
}
addTodo(text) {
this.todos.push({ text, completed: false });
}
removeTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id);
}
async fetchTodos() {
const response = await fetch('/api/todos');
const data = await response.json();
this.todos = data;
}
}
```
### 3. 使用 runInAction
```javascript
import { observable, runInAction } from 'mobx';
class TodoStore {
todos = [];
async fetchTodos() {
const response = await fetch('/api/todos');
const data = await response.json();
runInAction(() => {
this.todos = data;
});
}
}
```
## Action 的类型
### 1. action
最基础的 action 装饰器,用于包装状态修改方法。
```javascript
@action
updateTodo(id, updates) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
Object.assign(todo, updates);
}
}
```
### 2. action.bound
自动绑定 this 的 action,避免在回调函数中丢失 this 上下文。
```javascript
@action.bound
handleClick() {
this.count++;
}
// 使用时不需要 bind
<button onClick={this.handleClick}>Click</button>
```
### 3. runInAction
在异步操作中修改状态时使用。
```javascript
async loadData() {
const response = await fetch('/api/data');
const data = await response.json();
runInAction(() => {
this.data = data;
this.loading = false;
});
}
```
## Action 的最佳实践
### 1. 始终在 action 中修改状态
```javascript
// ❌ 错误:直接修改状态
class Store {
@observable count = 0;
increment() {
this.count++; // 不是 action
}
}
// ✅ 正确:在 action 中修改状态
class Store {
@observable count = 0;
@action increment() {
this.count++;
}
}
```
### 2. 使用 action.bound 处理事件处理器
```javascript
class Component {
@observable count = 0;
@action.bound handleClick() {
this.count++;
}
render() {
return <button onClick={this.handleClick}>Click</button>;
}
}
```
### 3. 异步操作中使用 runInAction
```javascript
@action
async fetchUser(id) {
this.loading = true;
try {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
runInAction(() => {
this.user = data;
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.error = error.message;
this.loading = false;
});
}
}
```
### 4. 合理拆分 action
```javascript
// ❌ 过于复杂的 action
@action
handleComplexOperation(data) {
this.loading = true;
this.data = data;
this.filtered = data.filter(item => item.active);
this.count = this.filtered.length;
this.timestamp = Date.now();
this.loading = false;
}
// ✅ 拆分为多个小 action
@action
handleComplexOperation(data) {
this.setLoading(true);
this.setData(data);
this.processData(data);
this.setLoading(false);
}
@action
setLoading(loading) {
this.loading = loading;
}
@action
setData(data) {
this.data = data;
}
@action
processData(data) {
this.filtered = data.filter(item => item.active);
this.count = this.filtered.length;
this.timestamp = Date.now();
}
```
## 常见错误
### 1. 在异步回调中直接修改状态
```javascript
// ❌ 错误
@action
async fetchData() {
const data = await fetch('/api/data');
this.data = await data.json(); // 不在 action 中
}
// ✅ 正确
@action
async fetchData() {
const data = await fetch('/api/data');
runInAction(() => {
this.data = data.json();
});
}
```
### 2. 忘记使用 action.bound
```javascript
// ❌ 错误:this 可能丢失
class Component {
@action handleClick() {
this.count++;
}
render() {
return <button onClick={this.handleClick}>Click</button>;
}
}
// ✅ 正确:使用 action.bound
class Component {
@action.bound handleClick() {
this.count++;
}
render() {
return <button onClick={this.handleClick}>Click</button>;
}
}
```
## 性能优化
1. **使用 action 批量更新**:减少 reaction 触发次数
2. **避免在循环中创建 action**:复用 action 函数
3. **合理使用 runInAction**:只在必要时使用
4. **使用 action.bound**:避免重复 bind
## 总结
- action 是修改 observable 状态的唯一推荐方式
- 使用 action 可以批量更新、提高性能、便于调试
- 优先使用 action.bound 处理事件处理器
- 异步操作中使用 runInAction 修改状态
- 合理拆分复杂的 action,提高可维护性
前端 · 2月22日 14:06
MobX 中 computed 的作用和使用场景有哪些?在 MobX 中,computed 是基于 observable 状态自动更新的派生值,类似于 Vue 的 computed 属性。computed 值会缓存计算结果,只有当依赖的 observable 状态发生变化时才会重新计算。
## Computed 的特性
1. **自动缓存**:计算结果会被缓存,避免重复计算
2. **懒计算**:只有在被访问时才会计算
3. **自动追踪依赖**:自动追踪依赖的 observable 状态
4. **自动更新**:依赖的状态变化时自动重新计算
5. **纯函数**:computed 应该是纯函数,不应该有副作用
## Computed 的使用方式
### 1. 使用装饰器
```javascript
import { observable, computed } from 'mobx';
class TodoStore {
@observable todos = [];
@observable filter = 'all';
@computed get completedTodos() {
return this.todos.filter(todo => todo.completed);
}
@computed get activeTodos() {
return this.todos.filter(todo => !todo.completed);
}
@computed get filteredTodos() {
switch (this.filter) {
case 'completed':
return this.completedTodos;
case 'active':
return this.activeTodos;
default:
return this.todos;
}
}
}
```
### 2. 使用 makeObservable
```javascript
import { makeObservable, observable, computed } from 'mobx';
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeObservable(this, {
todos: observable,
filter: observable,
completedTodos: computed,
activeTodos: computed,
filteredTodos: computed
});
}
get completedTodos() {
return this.todos.filter(todo => todo.completed);
}
get activeTodos() {
return this.todos.filter(todo => !todo.completed);
}
get filteredTodos() {
switch (this.filter) {
case 'completed':
return this.completedTodos;
case 'active':
return this.activeTodos;
default:
return this.todos;
}
}
}
```
### 3. 使用 makeAutoObservable(推荐)
```javascript
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeAutoObservable(this);
}
get completedTodos() {
return this.todos.filter(todo => todo.completed);
}
get activeTodos() {
return this.todos.filter(todo => !todo.completed);
}
get filteredTodos() {
switch (this.filter) {
case 'completed':
return this.completedTodos;
case 'active':
return this.activeTodos;
default:
return this.todos;
}
}
}
```
### 4. 使用 computed 函数
```javascript
import { observable, computed } from 'mobx';
const store = observable({
todos: [],
filter: 'all'
});
const completedTodos = computed(() =>
store.todos.filter(todo => todo.completed)
);
const activeTodos = computed(() =>
store.todos.filter(todo => !todo.completed)
);
console.log(completedTodos.get()); // 获取计算值
```
## Computed 的配置选项
### 1. name
为 computed 设置名称,便于调试。
```javascript
@computed({ name: 'completedTodos' })
get completedTodos() {
return this.todos.filter(todo => todo.completed);
}
```
### 2. equals
自定义比较函数,决定是否需要重新计算。
```javascript
@computed({
equals: (a, b) => a.length === b.length && a.every(item => b.includes(item))
})
get filteredTodos() {
return this.todos.filter(todo => todo.completed);
}
```
### 3. keepAlive
保持计算值活跃,即使没有被访问。
```javascript
@computed({ keepAlive: true })
get expensiveComputation() {
return this.largeArray.reduce((sum, item) => sum + item.value, 0);
}
```
## Computed 的最佳实践
### 1. 使用 computed 缓存复杂计算
```javascript
class Store {
@observable items = [];
@computed get totalValue() {
return this.items.reduce((sum, item) => sum + item.value, 0);
}
@computed get averageValue() {
return this.items.length > 0
? this.totalValue / this.items.length
: 0;
}
}
```
### 2. 避免在 computed 中产生副作用
```javascript
// ❌ 错误:computed 中有副作用
@computed get filteredItems() {
console.log('Filtering items'); // 副作用
return this.items.filter(item => item.active);
}
// ✅ 正确:使用 reaction 处理副作用
@computed get filteredItems() {
return this.items.filter(item => item.active);
}
constructor() {
reaction(
() => this.filteredItems,
(items) => console.log('Filtered items:', items)
);
}
```
### 3. 合理使用 computed 链
```javascript
class Store {
@observable products = [];
@observable category = 'all';
@observable minPrice = 0;
@observable maxPrice = Infinity;
@computed get filteredByCategory() {
return this.category === 'all'
? this.products
: this.products.filter(p => p.category === this.category);
}
@computed get filteredByPrice() {
return this.filteredByCategory.filter(
p => p.price >= this.minPrice && p.price <= this.maxPrice
);
}
@computed get sortedProducts() {
return [...this.filteredByPrice].sort((a, b) => a.price - b.price);
}
}
```
### 4. 使用 computed 处理表单验证
```javascript
class FormStore {
@observable username = '';
@observable email = '';
@observable password = '';
@computed get usernameError() {
if (this.username.length < 3) {
return 'Username must be at least 3 characters';
}
return '';
}
@computed get emailError() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(this.email)) {
return 'Invalid email format';
}
return '';
}
@computed get passwordError() {
if (this.password.length < 8) {
return 'Password must be at least 8 characters';
}
return '';
}
@computed get isValid() {
return !this.usernameError && !this.emailError && !this.passwordError;
}
}
```
## Computed vs Reaction
| 特性 | Computed | Reaction |
|------|----------|----------|
| 用途 | 计算派生值 | 执行副作用 |
| 返回值 | 返回计算结果 | 不返回值 |
| 缓存 | 自动缓存 | 不缓存 |
| 触发时机 | 被访问时 | 依赖变化时立即执行 |
| 适用场景 | 数据转换、过滤、聚合 | 日志、API 调用、DOM 操作 |
## 性能优化
1. **合理使用 computed**:避免过度使用,只在需要缓存时使用
2. **避免复杂计算**:如果计算非常耗时,考虑使用 memoization
3. **使用 computed 链**:将复杂计算拆分为多个小的 computed
4. **避免在 computed 中创建新对象**:可能导致不必要的重新计算
## 常见错误
### 1. 在 computed 中修改状态
```javascript
// ❌ 错误:computed 中修改状态
@computed get filteredItems() {
this.lastFilterTime = Date.now(); // 修改状态
return this.items.filter(item => item.active);
}
// ✅ 正确:使用 reaction 处理副作用
@computed get filteredItems() {
return this.items.filter(item => item.active);
}
constructor() {
reaction(
() => this.filteredItems,
() => {
this.lastFilterTime = Date.now();
}
);
}
```
### 2. 在 computed 中使用异步操作
```javascript
// ❌ 错误:computed 中使用异步操作
@computed async get userData() {
const response = await fetch('/api/user');
return response.json();
}
// ✅ 正确:使用 reaction 处理异步操作
constructor() {
reaction(
() => this.userId,
async (id) => {
const response = await fetch(`/api/user/${id}`);
runInAction(() => {
this.userData = await response.json();
});
}
);
}
```
## 总结
- computed 是基于 observable 状态自动更新的派生值
- computed 会自动缓存计算结果,提高性能
- computed 应该是纯函数,不应该有副作用
- 合理使用 computed 链处理复杂计算
- 避免在 computed 中修改状态或使用异步操作
- 使用 reaction 处理副作用和异步操作
前端 · 2月22日 14:05
MobX 中 observable 的使用方法和注意事项有哪些?在 MobX 中,observable 是核心概念之一,它将普通的 JavaScript 对象、数组、Map、Set 等数据结构转换为可观察对象,使 MobX 能够追踪其变化。
## Observable 的使用方式
### 1. 使用装饰器(Decorator)
```javascript
import { observable, action, computed } from 'mobx';
class TodoStore {
@observable todos = [];
@observable filter = 'all';
@computed get completedTodos() {
return this.todos.filter(todo => todo.completed);
}
@action addTodo(text) {
this.todos.push({ text, completed: false });
}
}
```
### 2. 使用 makeObservable
```javascript
import { makeObservable, observable, action, computed } from 'mobx';
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeObservable(this, {
todos: observable,
filter: observable,
completedTodos: computed,
addTodo: action
});
}
get completedTodos() {
return this.todos.filter(todo => todo.completed);
}
addTodo(text) {
this.todos.push({ text, completed: false });
}
}
```
### 3. 使用 makeAutoObservable(推荐)
```javascript
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeAutoObservable(this);
}
get completedTodos() {
return this.todos.filter(todo => todo.completed);
}
addTodo(text) {
this.todos.push({ text, completed: false });
}
}
```
### 4. 使用 observable 函数
```javascript
import { observable } from 'mobx';
const state = observable({
todos: [],
filter: 'all'
});
const completedTodos = observable(() =>
state.todos.filter(todo => todo.completed)
);
```
## Observable 的特性
### 1. 自动追踪依赖
当 observable 对象被读取时,MobX 会自动建立依赖关系。
```javascript
import { observable, autorun } from 'mobx';
const store = observable({
count: 0
});
autorun(() => {
console.log('Count is:', store.count);
});
store.count = 1; // 自动触发 autorun
```
### 2. 深度可观察
observable 默认是深度可观察的,会递归地将对象的所有属性都转换为可观察。
```javascript
const store = observable({
user: {
name: 'John',
address: {
city: 'New York'
}
}
});
store.user.address.city = 'Boston'; // 会被追踪
```
### 3. 数组和 Map 的特殊处理
observable 数组和 Map 提供了额外的 API,但保持与原生 API 兼容。
```javascript
const list = observable([1, 2, 3]);
list.push(4); // MobX 会追踪这个变化
list.splice(0, 1); // 也会被追踪
```
## Observable 配置选项
### 1. shallow(浅层可观察)
只追踪对象本身的变化,不追踪嵌套对象。
```javascript
import { observable } from 'mobx';
const store = observable({
user: { name: 'John' }
}, {}, { deep: false });
store.user = { name: 'Jane' }; // 会被追踪
store.user.name = 'Bob'; // 不会被追踪
```
### 2. asMap、asArray
显式指定数据结构类型。
```javascript
const map = observable.map({ key: 'value' });
const array = observable.array([1, 2, 3]);
```
## 注意事项
1. **不要在 action 外部修改 observable 状态**
2. **避免在 constructor 中使用装饰器**
3. **合理使用 computed 缓存计算结果**
4. **对于大型对象,考虑使用 shallow 优化性能**
5. **observable 对象不能被冻结(Object.freeze)**
## 最佳实践
1. 优先使用 `makeAutoObservable` 简化配置
2. 对于简单状态,使用 `observable` 函数
3. 对于复杂状态管理,使用类 + 装饰器或 `makeObservable`
4. 合理使用 computed 避免重复计算
5. 始终在 action 中修改 observable 状态
前端 · 2月22日 14:05
MobX 中 reaction 的类型和使用场景是什么?在 MobX 中,reaction 是用于处理副作用的机制,当 observable 状态发生变化时自动执行指定的函数。reaction 类似于 React 的 useEffect,但更加灵活和高效。
## Reaction 的类型
### 1. autorun
自动追踪依赖并在依赖变化时立即执行,适合需要立即执行的场景。
```javascript
import { observable, autorun } from 'mobx';
class TodoStore {
@observable todos = [];
constructor() {
autorun(() => {
console.log('Total todos:', this.todos.length);
// 保存到 localStorage
localStorage.setItem('todos', JSON.stringify(this.todos));
});
}
}
```
### 2. reaction
提供更细粒度的控制,可以指定追踪的数据和执行函数,适合需要控制执行时机的场景。
```javascript
import { observable, reaction } from 'mobx';
class TodoStore {
@observable todos = [];
@observable filter = 'all';
constructor() {
reaction(
() => this.todos.length, // 追踪的数据
(length) => {
console.log('Todo count changed:', length);
},
{ fireImmediately: false } // 配置选项
);
}
}
```
### 3. when
当条件满足时执行一次,然后自动清理,适合一次性操作。
```javascript
import { observable, when } from 'mobx';
class TodoStore {
@observable todos = [];
@observable loading = false;
constructor() {
when(
() => !this.loading && this.todos.length === 0,
() => {
console.log('Ready to load todos');
this.loadTodos();
}
);
}
@action loadTodos() {
this.loading = true;
// 加载数据...
}
}
```
## Reaction 的配置选项
### 1. fireImmediately
是否立即执行一次。
```javascript
reaction(
() => this.filter,
(filter) => {
console.log('Current filter:', filter);
},
{ fireImmediately: true } // 立即执行一次
);
```
### 2. delay
延迟执行,防抖效果。
```javascript
reaction(
() => this.searchQuery,
(query) => {
this.performSearch(query);
},
{ delay: 300 } // 延迟 300ms 执行
);
```
### 3. equals
自定义比较函数,决定是否触发 reaction。
```javascript
reaction(
() => this.items,
(items) => {
console.log('Items changed');
},
{
equals: (a, b) => {
return a.length === b.length && a.every(item => b.includes(item));
}
}
);
```
### 4. name
为 reaction 设置名称,便于调试。
```javascript
reaction(
() => this.todos,
(todos) => {
console.log('Todos updated');
},
{ name: 'saveTodosToLocalStorage' }
);
```
## Reaction 的使用场景
### 1. 数据持久化
```javascript
class TodoStore {
@observable todos = [];
constructor() {
// 从 localStorage 加载
this.todos = JSON.parse(localStorage.getItem('todos') || '[]');
// 保存到 localStorage
autorun(() => {
localStorage.setItem('todos', JSON.stringify(this.todos));
});
}
}
```
### 2. 日志记录
```javascript
class Store {
@observable user = null;
@observable actions = [];
constructor() {
reaction(
() => this.user,
(user) => {
console.log('User changed:', user);
this.actions.push({ type: 'USER_CHANGE', user, timestamp: Date.now() });
}
);
}
}
```
### 3. API 调用
```javascript
class ProductStore {
@observable categoryId = null;
@observable products = [];
@observable loading = false;
constructor() {
reaction(
() => this.categoryId,
async (categoryId) => {
if (categoryId) {
this.loading = true;
try {
const response = await fetch(`/api/products?category=${categoryId}`);
const data = await response.json();
runInAction(() => {
this.products = data;
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.loading = false;
});
}
}
}
);
}
}
```
### 4. 搜索防抖
```javascript
class SearchStore {
@observable query = '';
@observable results = [];
@observable loading = false;
constructor() {
reaction(
() => this.query,
async (query) => {
if (query.length > 2) {
this.loading = true;
try {
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
runInAction(() => {
this.results = data;
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.loading = false;
});
}
}
},
{ delay: 300 } // 防抖 300ms
);
}
}
```
### 5. 条件初始化
```javascript
class AppStore {
@observable initialized = false;
@observable user = null;
constructor() {
when(
() => this.initialized,
() => {
this.loadUserData();
}
);
}
@action
loadUserData() {
// 加载用户数据
}
}
```
## Reaction 的清理
### 1. 使用 dispose 清理
```javascript
class Component {
disposer = null;
componentDidMount() {
this.disposer = reaction(
() => this.store.todos,
(todos) => {
console.log('Todos changed:', todos);
}
);
}
componentWillUnmount() {
if (this.disposer) {
this.disposer(); // 清理 reaction
}
}
}
```
### 2. 使用 useEffect 清理
```javascript
import { useEffect } from 'react';
import { reaction } from 'mobx';
function TodoList({ store }) {
useEffect(() => {
const disposer = reaction(
() => store.todos,
(todos) => {
console.log('Todos changed:', todos);
}
);
return () => disposer(); // 清理 reaction
}, [store]);
return <div>{/* ... */}</div>;
}
```
## Reaction vs Computed
| 特性 | Reaction | Computed |
|------|----------|----------|
| 用途 | 执行副作用 | 计算派生值 |
| 返回值 | 不返回值 | 返回计算结果 |
| 缓存 | 不缓存 | 自动缓存 |
| 触发时机 | 依赖变化时立即执行 | 被访问时才计算 |
| 适用场景 | 日志、API 调用、DOM 操作 | 数据转换、过滤、聚合 |
## 最佳实践
### 1. 合理选择 reaction 类型
```javascript
// ✅ 使用 autorun:需要立即执行
autorun(() => {
console.log('Current state:', this.state);
});
// ✅ 使用 reaction:需要控制执行时机
reaction(
() => this.userId,
(id) => this.loadUser(id)
);
// ✅ 使用 when:一次性操作
when(
() => this.ready,
() => this.start()
);
```
### 2. 避免在 reaction 中修改依赖的状态
```javascript
// ❌ 错误:在 reaction 中修改依赖的状态
reaction(
() => this.count,
(count) => {
this.count = count + 1; // 会导致无限循环
}
);
// ✅ 正确:使用 action 修改其他状态
reaction(
() => this.count,
(count) => {
this.setCount(count + 1);
}
);
```
### 3. 使用 delay 防抖
```javascript
// ✅ 使用 delay 防抖,避免频繁触发
reaction(
() => this.searchQuery,
(query) => this.performSearch(query),
{ delay: 300 }
);
```
### 4. 记得清理 reaction
```javascript
// ✅ 在组件卸载时清理 reaction
useEffect(() => {
const disposer = reaction(
() => store.data,
(data) => console.log(data)
);
return () => disposer();
}, [store]);
```
## 常见错误
### 1. 忘记清理 reaction
```javascript
// ❌ 错误:忘记清理 reaction
class Component {
componentDidMount() {
reaction(() => this.store.data, (data) => {
console.log(data);
});
}
}
// ✅ 正确:清理 reaction
class Component {
disposer = null;
componentDidMount() {
this.disposer = reaction(() => this.store.data, (data) => {
console.log(data);
});
}
componentWillUnmount() {
if (this.disposer) {
this.disposer();
}
}
}
```
### 2. 在 reaction 中直接修改状态
```javascript
// ❌ 错误:在 reaction 中直接修改状态
reaction(
() => this.count,
(count) => {
this.count = count + 1; // 不在 action 中
}
);
// ✅ 正确:在 action 中修改状态
reaction(
() => this.count,
(count) => {
runInAction(() => {
this.count = count + 1;
});
}
);
```
## 总结
- reaction 是 MobX 中处理副作用的机制
- autorun 适合需要立即执行的场景
- reaction 提供更细粒度的控制
- when 适合一次性操作
- 使用 delay 可以实现防抖效果
- 记得在组件卸载时清理 reaction
- 避免在 reaction 中修改依赖的状态
- 使用 action 或 runInAction 修改状态
前端 · 2月22日 14:05
MobX 中如何处理异步操作?在 MobX 中,异步操作需要特别注意,因为状态变化必须在 action 中进行。MobX 提供了多种方式来处理异步操作。
## 处理异步操作的方式
### 1. 使用 runInAction
```javascript
import { observable, action, runInAction } from 'mobx';
class UserStore {
@observable users = [];
@observable loading = false;
@observable error = null;
@action
async fetchUsers() {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/users');
const data = await response.json();
runInAction(() => {
this.users = data;
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.error = error.message;
this.loading = false;
});
}
}
}
```
### 2. 使用 async action
```javascript
import { observable, action } from 'mobx';
class UserStore {
@observable users = [];
@observable loading = false;
@observable error = null;
@action.bound
async fetchUsers() {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/users');
const data = await response.json();
this.users = data;
this.loading = false;
} catch (error) {
this.error = error.message;
this.loading = false;
}
}
}
```
### 3. 使用 flow
```javascript
import { observable, flow } from 'mobx';
class UserStore {
@observable users = [];
@observable loading = false;
@observable error = null;
fetchUsers = flow(function* () {
this.loading = true;
this.error = null;
try {
const response = yield fetch('/api/users');
const data = yield response.json();
this.users = data;
this.loading = false;
} catch (error) {
this.error = error.message;
this.loading = false;
}
});
}
```
## 详细对比
### runInAction
**优点:**
- 灵活性高,可以在任何地方使用
- 适合处理复杂的异步逻辑
- 可以精确控制状态更新的时机
**缺点:**
- 需要手动包裹状态更新代码
- 代码结构可能不够清晰
**适用场景:**
- 需要在异步操作的不同阶段更新状态
- 复杂的异步逻辑
- 需要精确控制状态更新时机
```javascript
@action
async complexOperation() {
this.loading = true;
try {
const data1 = await this.fetchData1();
runInAction(() => {
this.data1 = data1;
});
const data2 = await this.fetchData2(data1.id);
runInAction(() => {
this.data2 = data2;
});
const result = await this.processData(data1, data2);
runInAction(() => {
this.result = result;
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.error = error.message;
this.loading = false;
});
}
}
```
### async action
**优点:**
- 代码结构清晰
- 自动处理状态更新
- 不需要手动包裹代码
**缺点:**
- 灵活性较低
- 所有状态更新都在同一个 action 中
**适用场景:**
- 简单的异步操作
- 不需要精确控制状态更新时机
- 代码结构优先的场景
```javascript
@action.bound
async simpleFetch() {
this.loading = true;
try {
const response = await fetch('/api/data');
const data = await response.json();
this.data = data;
this.loading = false;
} catch (error) {
this.error = error.message;
this.loading = false;
}
}
```
### flow
**优点:**
- 代码结构最清晰
- 自动处理状态更新
- 支持取消操作
- 更好的错误处理
**缺点:**
- 需要学习 generator 语法
- 兼容性问题(需要 polyfill)
**适用场景:**
- 复杂的异步流程
- 需要取消操作的场景
- 需要更好的错误处理
```javascript
fetchUsers = flow(function* () {
this.loading = true;
this.error = null;
try {
const response = yield fetch('/api/users');
const data = yield response.json();
this.users = data;
this.loading = false;
} catch (error) {
this.error = error.message;
this.loading = false;
}
});
// 可以取消 flow
const fetchTask = store.fetchUsers();
fetchTask.cancel();
```
## 最佳实践
### 1. 统一使用 async action
```javascript
class ApiStore {
@observable data = null;
@observable loading = false;
@observable error = null;
@action.bound
async fetchData(url) {
this.loading = true;
this.error = null;
try {
const response = await fetch(url);
const data = await response.json();
this.data = data;
this.loading = false;
} catch (error) {
this.error = error.message;
this.loading = false;
}
}
}
```
### 2. 使用 flow 处理复杂流程
```javascript
class OrderStore {
@observable orders = [];
@observable loading = false;
@observable error = null;
createOrder = flow(function* (orderData) {
this.loading = true;
this.error = null;
try {
// 验证订单
const validated = yield this.validateOrder(orderData);
// 创建订单
const order = yield this.createOrderApi(validated);
// 支付订单
const paid = yield this.payOrder(order.id);
// 更新状态
this.orders.push(paid);
this.loading = false;
return paid;
} catch (error) {
this.error = error.message;
this.loading = false;
throw error;
}
});
}
```
### 3. 使用 runInAction 处理分步更新
```javascript
class UploadStore {
@observable progress = 0;
@observable files = [];
@observable uploading = false;
@action
async uploadFiles(files) {
this.uploading = true;
this.progress = 0;
try {
for (let i = 0; i < files.length; i++) {
const file = files[i];
await this.uploadFile(file);
runInAction(() => {
this.files.push(file);
this.progress = ((i + 1) / files.length) * 100;
});
}
runInAction(() => {
this.uploading = false;
});
} catch (error) {
runInAction(() => {
this.uploading = false;
});
throw error;
}
}
}
```
### 4. 使用 reaction 处理自动加载
```javascript
class DataStore {
@observable userId = null;
@observable userData = null;
@observable loading = false;
constructor() {
reaction(
() => this.userId,
(userId) => {
if (userId) {
this.loadUserData(userId);
}
}
);
}
@action.bound
async loadUserData(userId) {
this.loading = true;
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
this.userData = data;
this.loading = false;
} catch (error) {
this.loading = false;
}
}
}
```
### 5. 错误处理和重试
```javascript
class Store {
@observable data = null;
@observable loading = false;
@observable error = null;
@observable retryCount = 0;
@action.bound
async fetchDataWithRetry(url, maxRetries = 3) {
this.loading = true;
this.error = null;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
runInAction(() => {
this.data = data;
this.loading = false;
this.retryCount = 0;
});
return data;
} catch (error) {
runInAction(() => {
this.retryCount = i + 1;
});
if (i === maxRetries - 1) {
runInAction(() => {
this.error = error.message;
this.loading = false;
});
throw error;
}
// 等待后重试
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
}
```
## 常见错误
### 1. 在 async 函数中直接修改状态
```javascript
// ❌ 错误:在 async 函数中直接修改状态
@action
async fetchData() {
this.loading = true;
const response = await fetch('/api/data');
const data = await response.json();
this.data = data; // 不在 action 中
this.loading = false;
}
// ✅ 正确:使用 runInAction 或 async action
@action
async fetchData() {
this.loading = true;
const response = await fetch('/api/data');
const data = await response.json();
runInAction(() => {
this.data = data;
this.loading = false;
});
}
// 或者
@action.bound
async fetchData() {
this.loading = true;
const response = await fetch('/api/data');
const data = await response.json();
this.data = data;
this.loading = false;
}
```
### 2. 忘记处理错误
```javascript
// ❌ 错误:忘记处理错误
@action
async fetchData() {
this.loading = true;
const response = await fetch('/api/data');
const data = await response.json();
runInAction(() => {
this.data = data;
this.loading = false;
});
}
// ✅ 正确:处理错误
@action
async fetchData() {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/data');
const data = await response.json();
runInAction(() => {
this.data = data;
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.error = error.message;
this.loading = false;
});
}
}
```
### 3. 忘记重置 loading 状态
```javascript
// ❌ 错误:忘记重置 loading 状态
@action
async fetchData() {
this.loading = true;
try {
const response = await fetch('/api/data');
const data = await response.json();
runInAction(() => {
this.data = data;
});
} catch (error) {
runInAction(() => {
this.error = error.message;
});
}
}
// ✅ 正确:在所有分支中重置 loading 状态
@action
async fetchData() {
this.loading = true;
try {
const response = await fetch('/api/data');
const data = await response.json();
runInAction(() => {
this.data = data;
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.error = error.message;
this.loading = false;
});
}
}
```
## 性能优化
### 1. 使用 debounce 防抖
```javascript
import { debounce } from 'lodash';
class SearchStore {
@observable query = '';
@observable results = [];
@observable loading = false;
constructor() {
this.debouncedSearch = debounce(this.performSearch.bind(this), 300);
}
@action
setQuery(query) {
this.query = query;
this.debouncedSearch();
}
@action.bound
async performSearch() {
if (this.query.length < 2) {
this.results = [];
return;
}
this.loading = true;
try {
const response = await fetch(`/api/search?q=${this.query}`);
const data = await response.json();
this.results = data;
this.loading = false;
} catch (error) {
this.loading = false;
}
}
}
```
### 2. 使用 requestAnimationFrame 优化 UI 更新
```javascript
@action
async loadData() {
this.loading = true;
const data = await this.fetchData();
// 使用 requestAnimationFrame 优化 UI 更新
requestAnimationFrame(() => {
runInAction(() => {
this.data = data;
this.loading = false;
});
});
}
```
## 总结
- 使用 async action 处理简单的异步操作
- 使用 runInAction 处理需要精确控制状态更新时机的场景
- 使用 flow 处理复杂的异步流程
- 始终处理错误和重置 loading 状态
- 使用 reaction 处理自动加载
- 使用 debounce 防抖优化性能
- 使用 requestAnimationFrame 优化 UI 更新
前端 · 2月22日 14:05
如何在 React 中使用 MobX?在 React 中使用 MobX 需要将 MobX 的响应式状态与 React 的渲染机制连接起来。MobX 提供了多种方式来实现这种集成。
## 安装依赖
```bash
npm install mobx mobx-react-lite
# 或者
npm install mobx mobx-react
```
## 使用方式
### 1. 使用 observer 高阶组件(mobx-react)
```javascript
import React from 'react';
import { observer } from 'mobx-react';
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this);
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
}
const todoStore = new TodoStore();
// 使用 observer 包装组件
const TodoList = observer(() => {
return (
<div>
<h1>Todo List</h1>
<ul>
{todoStore.todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => todoStore.toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
</li>
))}
</ul>
<button onClick={() => todoStore.addTodo('New Todo')}>
Add Todo
</button>
</div>
);
});
export default TodoList;
```
### 2. 使用 useObserver Hook(mobx-react-lite)
```javascript
import React from 'react';
import { useObserver } from 'mobx-react-lite';
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this);
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
}
const todoStore = new TodoStore();
function TodoList() {
return useObserver(() => (
<div>
<h1>Todo List</h1>
<ul>
{todoStore.todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => todoStore.toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
</li>
))}
</ul>
<button onClick={() => todoStore.addTodo('New Todo')}>
Add Todo
</button>
</div>
));
}
export default TodoList;
```
### 3. 使用 useLocalObservable Hook(mobx-react-lite)
```javascript
import React from 'react';
import { observer, useLocalObservable } from 'mobx-react-lite';
function TodoList() {
const store = useLocalObservable(() => ({
todos: [],
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
},
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
}));
return (
<div>
<h1>Todo List</h1>
<ul>
{store.todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => store.toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
</li>
))}
</ul>
<button onClick={() => store.addTodo('New Todo')}>
Add Todo
</button>
</div>
);
}
export default observer(TodoList);
```
### 4. 使用 React Context 共享 Store
```javascript
import React, { createContext, useContext } from 'react';
import { observer } from 'mobx-react';
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this);
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
}
const TodoContext = createContext(null);
function TodoProvider({ children }) {
const store = new TodoStore();
return (
<TodoContext.Provider value={store}>
{children}
</TodoContext.Provider>
);
}
function useTodoStore() {
const store = useContext(TodoContext);
if (!store) {
throw new Error('useTodoStore must be used within TodoProvider');
}
return store;
}
const TodoList = observer(() => {
const store = useTodoStore();
return (
<div>
<h1>Todo List</h1>
<ul>
{store.todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => store.toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
</li>
))}
</ul>
<button onClick={() => store.addTodo('New Todo')}>
Add Todo
</button>
</div>
);
});
function App() {
return (
<TodoProvider>
<TodoList />
</TodoProvider>
);
}
export default App;
```
### 5. 使用 Provider 和 inject(mobx-react 旧版)
```javascript
import React from 'react';
import { Provider, observer, inject } from 'mobx-react';
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this);
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
}
const todoStore = new TodoStore();
@inject('todoStore')
@observer
class TodoList extends React.Component {
render() {
const { todoStore } = this.props;
return (
<div>
<h1>Todo List</h1>
<ul>
{todoStore.todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => todoStore.toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
</li>
))}
</ul>
<button onClick={() => todoStore.addTodo('New Todo')}>
Add Todo
</button>
</div>
);
}
}
function App() {
return (
<Provider todoStore={todoStore}>
<TodoList />
</Provider>
);
}
export default App;
```
## 最佳实践
### 1. 使用 observer 包裹需要响应式更新的组件
```javascript
// ✅ 正确:只包裹需要响应式更新的组件
const TodoItem = observer(({ todo }) => (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => todo.toggle()}
/>
<span>{todo.text}</span>
</li>
));
const TodoList = ({ store }) => (
<ul>
{store.todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
// ❌ 错误:包裹整个应用,可能导致不必要的渲染
const App = observer(() => (
<div>
<Header />
<TodoList />
<Footer />
</div>
));
```
### 2. 合理拆分 Store
```javascript
// ✅ 正确:按功能拆分 Store
class TodoStore {
@observable todos = [];
@observable filter = 'all';
@action addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
}
}
class UserStore {
@observable user = null;
@action setUser(user) {
this.user = user;
}
}
class AppStore {
todoStore = new TodoStore();
userStore = new UserStore();
}
// 使用 Context 共享
const StoreContext = createContext(new AppStore());
```
### 3. 使用 computed 优化性能
```javascript
class TodoStore {
@observable todos = [];
@observable filter = 'all';
@computed get filteredTodos() {
switch (this.filter) {
case 'completed':
return this.todos.filter(todo => todo.completed);
case 'active':
return this.todos.filter(todo => !todo.completed);
default:
return this.todos;
}
}
}
const TodoList = observer(({ store }) => (
<ul>
{store.filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
));
```
### 4. 使用 React.memo 优化子组件
```javascript
const TodoItem = observer(React.memo(({ todo }) => (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => todo.toggle()}
/>
<span>{todo.text}</span>
</li>
)));
```
### 5. 使用 useEffect 清理副作用
```javascript
import { useEffect } from 'react';
import { reaction } from 'mobx';
const TodoList = observer(({ store }) => {
useEffect(() => {
const disposer = reaction(
() => store.todos.length,
(length) => {
console.log('Todo count changed:', length);
}
);
return () => disposer();
}, [store]);
return (
<ul>
{store.todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
});
```
## 常见问题
### 1. 组件不更新
```javascript
// ❌ 错误:忘记使用 observer
function TodoList({ store }) {
return (
<ul>
{store.todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
// ✅ 正确:使用 observer
const TodoList = observer(({ store }) => (
<ul>
{store.todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
));
```
### 2. 在 observer 组件外修改状态
```javascript
// ❌ 错误:在 observer 组件外直接修改状态
function App() {
const store = useStore();
useEffect(() => {
store.todos.push({ id: 1, text: 'Todo' }); // 不在 action 中
}, []);
return <TodoList store={store} />;
}
// ✅ 正确:在 action 中修改状态
function App() {
const store = useStore();
useEffect(() => {
store.addTodo('Todo'); // 在 action 中
}, []);
return <TodoList store={store} />;
}
```
### 3. 过度使用 observer
```javascript
// ❌ 错误:过度使用 observer
const Header = observer(() => <header>Header</header>);
const Footer = observer(() => <footer>Footer</footer>);
const Main = observer(() => <main>Main</main>);
// ✅ 正确:只在需要响应式更新的组件使用 observer
const Header = () => <header>Header</header>;
const Footer = () => <footer>Footer</footer>;
const Main = observer(() => <main>Main</main>);
```
## 总结
- 使用 observer 或 useObserver 将组件连接到 MobX
- 使用 React Context 共享 Store
- 合理拆分 Store,按功能模块组织
- 使用 computed 优化性能
- 使用 React.memo 优化子组件
- 记得在 useEffect 中清理 reaction
- 避免过度使用 observer
- 始终在 action 中修改状态
服务端 · 2月22日 14:05
MobX 6 有哪些主要变化和新特性?MobX 6 是 MobX 的一个重大版本更新,引入了许多重要的变化和新特性。以下是 MobX 6 的主要变化:
## 1. 强制使用 Action
### MobX 5 及之前
```javascript
class Store {
@observable count = 0;
increment() {
this.count++; // 可以直接修改
}
}
```
### MobX 6
```javascript
class Store {
@observable count = 0;
@action // 必须使用 action
increment() {
this.count++;
}
}
```
在 MobX 6 中,所有状态修改都必须在 action 中进行。这是为了提供更好的可预测性和调试体验。
## 2. 装饰器 API 的变化
### MobX 5
```javascript
import { observable, computed, action } from 'mobx';
class Store {
@observable count = 0;
@computed get doubled() {
return this.count * 2;
}
@action increment() {
this.count++;
}
}
```
### MobX 6
```javascript
import { makeObservable, observable, computed, action } from 'mobx';
class Store {
count = 0;
constructor() {
makeObservable(this);
}
get doubled() {
return this.count * 2;
}
increment() {
this.count++;
}
}
```
MobX 6 推荐使用 `makeObservable` 而不是装饰器,但装饰器仍然支持。
## 3. makeAutoObservable 的引入
MobX 6 引入了 `makeAutoObservable`,它可以自动推断属性的类型:
```javascript
import { makeAutoObservable } from 'mobx';
class Store {
count = 0;
firstName = 'John';
lastName = 'Doe';
constructor() {
makeAutoObservable(this);
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
increment() {
this.count++;
}
}
```
`makeAutoObservable` 会自动:
- 将 getter 标记为 computed
- 将方法标记为 action
- 将字段标记为 observable
## 4. 配置 API 的简化
### MobX 5
```javascript
import { configure } from 'mobx';
configure({
enforceActions: 'always',
useProxies: 'always',
computedRequiresReaction: true
});
```
### MobX 6
```javascript
import { configure } from 'mobx';
configure({
enforceActions: 'always', // 默认值
useProxies: 'ifavailable', // 默认值
computedRequiresReaction: false // 默认值
});
```
MobX 6 的默认配置更加严格和合理。
## 5. Proxy 的强制使用
MobX 6 强制使用 Proxy(在支持的浏览器中),这提供了更好的性能和更简单的 API。
### Proxy 的优势
- 更好的性能
- 更简单的 API
- 更好的 TypeScript 支持
- 更少的限制
### 不支持 Proxy 的环境
在旧浏览器或 Node.js 环境中,MobX 6 会自动降级到兼容模式。
## 6. 移除的 API
以下 API 在 MobX 6 中被移除:
- `isObservableObject` → 使用 `isObservable`
- `intercept` → 使用 `observe` 的拦截功能
- `extras` API → 大部分功能已集成到主 API
- `toJS` → 使用 `toJSON` 或手动转换
## 7. TypeScript 支持的改进
MobX 6 对 TypeScript 的支持更加完善:
```typescript
import { makeAutoObservable } from 'mobx';
class Store {
count: number = 0;
firstName: string = 'John';
lastName: string = 'Doe';
constructor() {
makeAutoObservable(this);
}
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
increment(): void {
this.count++;
}
}
```
类型推断更加准确,类型定义更加简洁。
## 8. 性能优化
MobX 6 引入了多项性能优化:
- 更快的依赖追踪
- 更高效的 computed 缓存
- 更好的批量更新机制
- 减少的内存占用
## 9. 调试体验的改进
MobX 6 提供了更好的调试工具:
- 更清晰的错误消息
- 更好的堆栈跟踪
- 改进的 DevTools 支持
## 10. 迁移指南
### 从 MobX 5 迁移到 MobX 6
1. **更新依赖**
```bash
npm install mobx@6 mobx-react@6
```
2. **添加 action 装饰器**
```javascript
// 之前
increment() {
this.count++;
}
// 之后
@action
increment() {
this.count++;
}
```
3. **使用 makeObservable 或 makeAutoObservable**
```javascript
constructor() {
makeAutoObservable(this);
}
```
4. **更新配置**
```javascript
configure({
enforceActions: 'always'
});
```
5. **移除已废弃的 API**
- 替换 `toJS` 为 `toJSON`
- 更新 `isObservableObject` 为 `isObservable`
## 总结
MobX 6 是一个重要的版本更新,主要改进包括:
- 强制使用 action 提高可预测性
- 简化的 API 和更好的 TypeScript 支持
- 强制使用 Proxy 提高性能
- 更好的调试体验
- 移除已废弃的 API
迁移到 MobX 6 需要一些工作,但带来的改进是值得的。建议新项目直接使用 MobX 6,现有项目逐步迁移。
前端 · 2月21日 15:51