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

MobX 中 reaction 的类型和使用场景是什么?

2月22日 14:05

在 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

特性ReactionComputed
用途执行副作用计算派生值
返回值不返回值返回计算结果
缓存不缓存自动缓存
触发时机依赖变化时立即执行被访问时才计算
适用场景日志、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 修改状态
标签:Mobx