Reaction反应 #

reaction 是 MobX 提供的一个更精确的响应式函数。与 autorun 不同,reaction 允许你明确指定要追踪的状态,以及状态变化时要执行的副作用。

基本用法 #

简单示例 #

javascript
import { makeAutoObservable, reaction } from 'mobx';

class Store {
  count = 0;
  name = 'MobX';

  constructor() {
    makeAutoObservable(this);
  }
}

const store = new Store();

// reaction(追踪函数, 副作用函数)
const dispose = reaction(
  // 追踪函数:返回要追踪的值
  () => store.count,
  // 副作用函数:值变化时执行
  (count, previousCount) => {
    console.log(`Count changed from ${previousCount} to ${count}`);
  }
);

store.count = 1;  // 输出: Count changed from 0 to 1
store.count = 2;  // 输出: Count changed from 1 to 2

// name 变化不会触发,因为没有被追踪
store.name = 'React';  // 没有输出

工作原理 #

text
┌─────────────────────────────────────────────────────────────┐
│                    reaction 工作流程                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   reaction(                                                 │
│     () => trackedValue,    // 1. 追踪函数                    │
│     (value, prev) => { }   // 2. 副作用函数                  │
│   )                                                         │
│                                                             │
│   执行流程:                                                 │
│   1. 执行追踪函数,收集依赖                                  │
│   2. 追踪函数返回值变化时,执行副作用函数                     │
│   3. 副作用函数接收新值和旧值                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

与autorun的区别 #

javascript
import { autorun, reaction } from 'mobx';

// autorun:追踪所有访问的状态
autorun(() => {
  console.log(`Count: ${store.count}, Name: ${store.name}`);
});
// count 或 name 变化都会触发

// reaction:只追踪返回的状态
reaction(
  () => store.count,
  (count) => {
    console.log(`Count: ${count}`);
  }
);
// 只有 count 变化才触发

选项配置 #

fireImmediately #

立即执行一次副作用函数:

javascript
reaction(
  () => store.count,
  (count) => {
    console.log(`Count: ${count}`);
  },
  { fireImmediately: true }
);
// 立即输出: Count: 0

delay #

设置延迟执行(防抖):

javascript
reaction(
  () => store.searchQuery,
  (query) => {
    fetchResults(query);
  },
  { delay: 300 }  // 延迟 300ms
);

equals #

自定义比较函数:

javascript
import { comparer } from 'mobx';

// 结构比较
reaction(
  () => store.position,
  (pos) => {
    console.log('Position changed:', pos);
  },
  { equals: comparer.structural }
);

// 自定义比较
reaction(
  () => store.items.length,
  (length) => {
    console.log('Items count:', length);
  },
  { equals: (a, b) => a === b }
);

name #

设置名称便于调试:

javascript
reaction(
  () => store.count,
  (count) => console.log(count),
  { name: 'countWatcher' }
);

使用场景 #

精确追踪特定状态 #

javascript
import { reaction } from 'mobx';

// 只追踪用户登录状态
reaction(
  () => store.isLoggedIn,
  (isLoggedIn) => {
    if (isLoggedIn) {
      console.log('User logged in');
      loadUserData();
    } else {
      console.log('User logged out');
      clearUserData();
    }
  }
);

追踪对象属性 #

javascript
// 追踪对象的特定属性
reaction(
  () => ({ name: store.user.name, email: store.user.email }),
  (userInfo) => {
    saveUserInfo(userInfo);
  }
);

追踪数组长度 #

javascript
// 只追踪数组长度变化
reaction(
  () => store.items.length,
  (length, previousLength) => {
    console.log(`Items: ${previousLength} -> ${length}`);
  }
);

追踪计算值 #

javascript
// 追踪计算值变化
reaction(
  () => store.filteredItems,
  (items) => {
    updateUI(items);
  }
);

完整示例 #

搜索功能 #

javascript
import { makeAutoObservable, reaction, runInAction } from 'mobx';

class SearchStore {
  query = '';
  results = [];
  loading = false;

  constructor() {
    makeAutoObservable(this);

    // 使用 reaction 实现搜索
    reaction(
      () => this.query,
      async (query) => {
        if (!query.trim()) {
          this.results = [];
          return;
        }

        this.loading = true;
        try {
          const response = await fetch(`/api/search?q=${query}`);
          const results = await response.json();
          runInAction(() => {
            this.results = results;
            this.loading = false;
          });
        } catch (error) {
          runInAction(() => {
            this.loading = false;
          });
        }
      },
      { delay: 300 }  // 防抖
    );
  }

  setQuery(query) {
    this.query = query;
  }
}

export default new SearchStore();

购物车变化通知 #

javascript
import { makeAutoObservable, reaction } from 'mobx';

class CartStore {
  items = [];

  constructor() {
    makeAutoObservable(this);

    // 监听购物车变化
    reaction(
      () => this.items.length,
      (count, previousCount) => {
        if (count > previousCount) {
          this.showNotification('Item added to cart');
        } else if (count < previousCount) {
          this.showNotification('Item removed from cart');
        }
      }
    );

    // 监听总价变化
    reaction(
      () => this.total,
      (total) => {
        this.updateFreeShippingStatus(total);
      }
    );
  }

  get total() {
    return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }

  showNotification(message) {
    console.log(message);
  }

  updateFreeShippingStatus(total) {
    if (total >= 100) {
      console.log('You qualify for free shipping!');
    }
  }

  addItem(item) {
    this.items.push(item);
  }

  removeItem(id) {
    this.items = this.items.filter(i => i.id !== id);
  }
}

export default new CartStore();

路由变化处理 #

javascript
import { makeAutoObservable, reaction } from 'mobx';

class RouterStore {
  currentPath = window.location.pathname;
  params = {};

  constructor() {
    makeAutoObservable(this);

    // 监听路由变化
    reaction(
      () => this.currentPath,
      (path, previousPath) => {
        console.log(`Navigation: ${previousPath} -> ${path}`);
        this.handleRouteChange(path);
      }
    );

    // 监听 popstate 事件
    window.addEventListener('popstate', () => {
      this.currentPath = window.location.pathname;
    });
  }

  handleRouteChange(path) {
    // 根据路径加载不同内容
    switch (path) {
      case '/':
        this.loadHomePage();
        break;
      case '/about':
        this.loadAboutPage();
        break;
      default:
        this.loadNotFoundPage();
    }
  }

  navigate(path) {
    window.history.pushState({}, '', path);
    this.currentPath = path;
  }

  loadHomePage() { }
  loadAboutPage() { }
  loadNotFoundPage() { }
}

export default new RouterStore();

主题切换 #

javascript
import { makeAutoObservable, reaction } from 'mobx';

class ThemeStore {
  theme = localStorage.getItem('theme') || 'light';

  constructor() {
    makeAutoObservable(this);

    // 监听主题变化,更新 DOM 和保存设置
    reaction(
      () => this.theme,
      (theme) => {
        document.documentElement.setAttribute('data-theme', theme);
        localStorage.setItem('theme', theme);
      },
      { fireImmediately: true }
    );
  }

  toggleTheme() {
    this.theme = this.theme === 'light' ? 'dark' : 'light';
  }

  setTheme(theme) {
    this.theme = theme;
  }
}

export default new ThemeStore();

高级用法 #

追踪多个值 #

javascript
reaction(
  () => ({
    count: store.count,
    name: store.name,
    active: store.active
  }),
  ({ count, name, active }) => {
    console.log('State changed:', { count, name, active });
  }
);

条件追踪 #

javascript
reaction(
  () => store.isLoggedIn ? store.user.id : null,
  (userId) => {
    if (userId) {
      loadUserData(userId);
    }
  }
);

错误处理 #

javascript
reaction(
  () => store.data,
  async (data) => {
    try {
      await saveData(data);
    } catch (error) {
      console.error('Save failed:', error);
    }
  }
);

reaction vs autorun #

特性 reaction autorun
追踪方式 显式指定 自动追踪
立即执行 否(默认)
旧值访问 支持 不支持
精确控制

选择建议:

  • 需要精确控制追踪:使用 reaction
  • 需要立即执行:使用 autorunreaction({ fireImmediately: true })
  • 需要访问旧值:使用 reaction
  • 简单副作用:使用 autorun

总结 #

reaction 的核心要点:

  • 精确追踪:只追踪返回的状态
  • 延迟执行:默认不立即执行
  • 访问旧值:可以获取变化前后的值
  • 灵活配置:支持多种选项

使用场景:

  • 精确追踪特定状态变化
  • 需要访问旧值的场景
  • 条件触发的副作用
  • 防抖处理

继续学习 When条件执行,了解条件触发的响应式函数。

最后更新:2026-03-28