异步Action #

在 MobX 中处理异步操作需要特别注意,因为异步代码中的状态修改需要正确地包装在 action 中。MobX 提供了多种处理异步操作的方式。

异步操作的问题 #

问题示例 #

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

configure({ enforceActions: 'always' });

class Store {
  data = null;
  loading = false;

  constructor() {
    makeAutoObservable(this);
  }

  async fetchData() {
    this.loading = true;  // OK:在 action 中

    const response = await fetch('/api/data');
    const data = await response.json();

    // Error:不在 action 中!
    this.data = data;
    this.loading = false;
  }
}

解决方案 #

方案1:runInAction #

使用 runInAction 包装状态修改:

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

class Store {
  data = null;
  loading = false;
  error = null;

  constructor() {
    makeAutoObservable(this);
  }

  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;
      });
    }
  }
}

方案2:flow(推荐) #

使用 flow 处理异步操作:

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

class Store {
  data = null;
  loading = false;
  error = null;

  constructor() {
    makeAutoObservable(this);
  }

  // 使用 flow 处理异步
  fetchData = flow(function* () {
    this.loading = true;
    this.error = null;

    try {
      const response = yield fetch('/api/data');
      const data = yield response.json();

      this.data = data;
      this.error = null;
    } catch (error) {
      this.error = error.message;
    } finally {
      this.loading = false;
    }
  });
}

方案3:action包装 #

将整个异步函数包装在 action 中:

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

class Store {
  data = null;
  loading = false;

  constructor() {
    makeAutoObservable(this);
  }

  fetchData = action(async () => {
    this.loading = true;

    const response = await fetch('/api/data');
    const data = await response.json();

    // 注意:这种方式在 enforceActions: 'always' 下仍然会报错
    this.data = data;
    this.loading = false;
  });
}

flow详解 #

基本语法 #

javascript
import { flow } from 'mobx';

class Store {
  constructor() {
    makeAutoObservable(this);
  }

  // flow 返回一个 Promise
  asyncAction = flow(function* (param) {
    // yield 等待 Promise
    const result = yield someAsyncOperation(param);

    // 直接修改状态
    this.data = result;

    return result;
  });
}

flow的特点 #

  1. 自动包装:yield 后的代码自动在 action 中
  2. 可取消:返回的 Promise 有 cancel 方法
  3. 错误处理:使用 try/catch 处理错误
javascript
class Store {
  fetchData = flow(function* (id) {
    try {
      const response = yield fetch(`/api/data/${id}`);
      const data = yield response.json();
      this.data = data;
      return data;
    } catch (error) {
      this.error = error.message;
      throw error;
    }
  });
}

// 使用
const store = new Store();
const promise = store.fetchData(1);

// 取消操作
promise.cancel();

完整示例 #

API客户端 #

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

class ApiStore {
  data = null;
  loading = false;
  error = null;

  constructor() {
    makeAutoObservable(this);
  }

  // GET 请求
  get = flow(function* (url) {
    this.loading = true;
    this.error = null;

    try {
      const response = yield fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const data = yield response.json();
      this.data = data;
      return data;
    } catch (error) {
      this.error = error.message;
      throw error;
    } finally {
      this.loading = false;
    }
  });

  // POST 请求
  post = flow(function* (url, body) {
    this.loading = true;
    this.error = null;

    try {
      const response = yield fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body)
      });
      const data = yield response.json();
      this.data = data;
      return data;
    } catch (error) {
      this.error = error.message;
      throw error;
    } finally {
      this.loading = false;
    }
  });

  // 并行请求
  fetchAll = flow(function* (urls) {
    this.loading = true;

    try {
      const responses = yield Promise.all(
        urls.map(url => fetch(url).then(r => r.json()))
      );
      this.data = responses;
      return responses;
    } catch (error) {
      this.error = error.message;
      throw error;
    } finally {
      this.loading = false;
    }
  });

  clear() {
    this.data = null;
    this.error = null;
  }
}

export default new ApiStore();

用户认证 #

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

class AuthStore {
  user = null;
  token = null;
  loading = false;
  error = null;

  constructor() {
    makeAutoObservable(this);
  }

  // 登录
  login = flow(function* (credentials) {
    this.loading = true;
    this.error = null;

    try {
      const response = yield fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      });

      if (!response.ok) {
        throw new Error('Invalid credentials');
      }

      const data = yield response.json();

      this.user = data.user;
      this.token = data.token;
      localStorage.setItem('token', data.token);

      return data;
    } catch (error) {
      this.error = error.message;
      throw error;
    } finally {
      this.loading = false;
    }
  });

  // 登出
  logout = flow(function* () {
    try {
      yield fetch('/api/logout', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.token}`
        }
      });
    } finally {
      this.user = null;
      this.token = null;
      localStorage.removeItem('token');
    }
  });

  // 检查登录状态
  checkAuth = flow(function* () {
    const token = localStorage.getItem('token');
    if (!token) return;

    this.loading = true;

    try {
      const response = yield fetch('/api/me', {
        headers: { 'Authorization': `Bearer ${token}` }
      });

      if (response.ok) {
        const user = yield response.json();
        this.user = user;
        this.token = token;
      } else {
        this.logout();
      }
    } catch (error) {
      this.logout();
    } finally {
      this.loading = false;
    }
  });

  get isLoggedIn() {
    return this.user !== null;
  }
}

export default new AuthStore();

分页加载 #

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

class PaginationStore {
  items = [];
  page = 1;
  pageSize = 10;
  total = 0;
  loading = false;
  hasMore = true;

  constructor() {
    makeAutoObservable(this);
  }

  loadMore = flow(function* (fetchFn) {
    if (this.loading || !this.hasMore) return;

    this.loading = true;

    try {
      const response = yield fetchFn(this.page, this.pageSize);

      this.items = [...this.items, ...response.items];
      this.total = response.total;
      this.page++;
      this.hasMore = this.items.length < this.total;
    } catch (error) {
      console.error('Load more failed:', error);
    } finally {
      this.loading = false;
    }
  });

  refresh = flow(function* (fetchFn) {
    this.loading = true;

    try {
      const response = yield fetchFn(1, this.pageSize);

      this.items = response.items;
      this.total = response.total;
      this.page = 2;
      this.hasMore = this.items.length < this.total;
    } catch (error) {
      console.error('Refresh failed:', error);
    } finally {
      this.loading = false;
    }
  });

  reset() {
    this.items = [];
    this.page = 1;
    this.total = 0;
    this.hasMore = true;
  }
}

最佳实践 #

1. 错误处理 #

javascript
class Store {
  fetchData = flow(function* () {
    this.loading = true;
    this.error = null;

    try {
      const response = yield fetch('/api/data');
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      const data = yield response.json();
      this.data = data;
    } catch (error) {
      this.error = error.message;
      // 可以选择重新抛出或处理
    } finally {
      this.loading = false;
    }
  });
}

2. 取消操作 #

javascript
class Store {
  currentRequest = null;

  search = flow(function* (query) {
    // 取消之前的请求
    if (this.currentRequest) {
      this.currentRequest.cancel();
    }

    this.loading = true;

    try {
      const promise = fetch(`/api/search?q=${query}`);
      this.currentRequest = promise;

      const response = yield promise;
      const data = yield response.json();
      this.results = data;
    } catch (error) {
      if (error.name !== 'CancellationError') {
        this.error = error.message;
      }
    } finally {
      this.loading = false;
      this.currentRequest = null;
    }
  });
}

3. 重试机制 #

javascript
class Store {
  fetchWithRetry = flow(function* (url, retries = 3) {
    for (let i = 0; i < retries; i++) {
      try {
        const response = yield fetch(url);
        if (response.ok) {
          return yield response.json();
        }
      } catch (error) {
        if (i === retries - 1) throw error;
        yield new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
      }
    }
  });
}

总结 #

方式 优点 缺点
runInAction 灵活,可嵌套 代码稍显冗余
flow 代码简洁,可取消 需要 Generator
action包装 简单 不完全符合规范

推荐使用 flow 处理复杂的异步操作,简单场景可以使用 runInAction

继续学习 Observable集合,了解数组和Map的响应式处理。

最后更新:2026-03-28