乐闻世界logo
搜索文章和话题

Mobx

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