在 MobX 中,reaction 是用于处理副作用的机制,当 observable 状态发生变化时自动执行指定的函数。reaction 类似于 React 的 useEffect,但更加灵活和高效。
Reaction 的类型
1. autorun
自动追踪依赖并在依赖变化时立即执行,适合需要立即执行的场景。
javascriptimport { 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
提供更细粒度的控制,可以指定追踪的数据和执行函数,适合需要控制执行时机的场景。
javascriptimport { 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
当条件满足时执行一次,然后自动清理,适合一次性操作。
javascriptimport { 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
是否立即执行一次。
javascriptreaction( () => this.filter, (filter) => { console.log('Current filter:', filter); }, { fireImmediately: true } // 立即执行一次 );
2. delay
延迟执行,防抖效果。
javascriptreaction( () => this.searchQuery, (query) => { this.performSearch(query); }, { delay: 300 } // 延迟 300ms 执行 );
3. equals
自定义比较函数,决定是否触发 reaction。
javascriptreaction( () => this.items, (items) => { console.log('Items changed'); }, { equals: (a, b) => { return a.length === b.length && a.every(item => b.includes(item)); } } );
4. name
为 reaction 设置名称,便于调试。
javascriptreaction( () => this.todos, (todos) => { console.log('Todos updated'); }, { name: 'saveTodosToLocalStorage' } );
Reaction 的使用场景
1. 数据持久化
javascriptclass TodoStore { @observable todos = []; constructor() { // 从 localStorage 加载 this.todos = JSON.parse(localStorage.getItem('todos') || '[]'); // 保存到 localStorage autorun(() => { localStorage.setItem('todos', JSON.stringify(this.todos)); }); } }
2. 日志记录
javascriptclass 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 调用
javascriptclass 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. 搜索防抖
javascriptclass 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. 条件初始化
javascriptclass AppStore { @observable initialized = false; @observable user = null; constructor() { when( () => this.initialized, () => { this.loadUserData(); } ); } @action loadUserData() { // 加载用户数据 } }
Reaction 的清理
1. 使用 dispose 清理
javascriptclass 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 清理
javascriptimport { 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 修改状态