异步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的特点 #
- 自动包装:yield 后的代码自动在 action 中
- 可取消:返回的 Promise 有 cancel 方法
- 错误处理:使用 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