MobX 中 reaction 的类型和使用场景是什么?
在 MobX 中,reaction 是用于处理副作用的机制,当 observable 状态发生变化时自动执行指定的函数。reaction 类似于 React 的 useEffect,但更加灵活和高效。Reaction 的类型1. autorun自动追踪依赖并在依赖变化时立即执行,适合需要立即执行的场景。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提供更细粒度的控制,可以指定追踪的数据和执行函数,适合需要控制执行时机的场景。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当条件满足时执行一次,然后自动清理,适合一次性操作。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是否立即执行一次。reaction( () => this.filter, (filter) => { console.log('Current filter:', filter); }, { fireImmediately: true } // 立即执行一次);2. delay延迟执行,防抖效果。reaction( () => this.searchQuery, (query) => { this.performSearch(query); }, { delay: 300 } // 延迟 300ms 执行);3. equals自定义比较函数,决定是否触发 reaction。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 设置名称,便于调试。reaction( () => this.todos, (todos) => { console.log('Todos updated'); }, { name: 'saveTodosToLocalStorage' });Reaction 的使用场景1. 数据持久化class TodoStore { @observable todos = []; constructor() { // 从 localStorage 加载 this.todos = JSON.parse(localStorage.getItem('todos') || '[]'); // 保存到 localStorage autorun(() => { localStorage.setItem('todos', JSON.stringify(this.todos)); }); }}2. 日志记录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 调用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. 搜索防抖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. 条件初始化class AppStore { @observable initialized = false; @observable user = null; constructor() { when( () => this.initialized, () => { this.loadUserData(); } ); } @action loadUserData() { // 加载用户数据 }}Reaction 的清理1. 使用 dispose 清理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 清理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 类型// ✅ 使用 autorun:需要立即执行autorun(() => { console.log('Current state:', this.state);});// ✅ 使用 reaction:需要控制执行时机reaction( () => this.userId, (id) => this.loadUser(id));// ✅ 使用 when:一次性操作when( () => this.ready, () => this.start());2. 避免在 reaction 中修改依赖的状态// ❌ 错误:在 reaction 中修改依赖的状态reaction( () => this.count, (count) => { this.count = count + 1; // 会导致无限循环 });// ✅ 正确:使用 action 修改其他状态reaction( () => this.count, (count) => { this.setCount(count + 1); });3. 使用 delay 防抖// ✅ 使用 delay 防抖,避免频繁触发reaction( () => this.searchQuery, (query) => this.performSearch(query), { delay: 300 });4. 记得清理 reaction// ✅ 在组件卸载时清理 reactionuseEffect(() => { const disposer = reaction( () => store.data, (data) => console.log(data) ); return () => disposer();}, [store]);常见错误1. 忘记清理 reaction// ❌ 错误:忘记清理 reactionclass Component { componentDidMount() { reaction(() => this.store.data, (data) => { console.log(data); }); }}// ✅ 正确:清理 reactionclass Component { disposer = null; componentDidMount() { this.disposer = reaction(() => this.store.data, (data) => { console.log(data); }); } componentWillUnmount() { if (this.disposer) { this.disposer(); } }}2. 在 reaction 中直接修改状态// ❌ 错误:在 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 修改状态