本文详细讲解在 dva 状态管理框架中如何发起异步请求。通过剖析 Model 中的 effects 机制,结合 call 与 put 两个核心操作符,从 API 接口封装、effects 异步调用、reducers 同步更新到组件 dispatch 与 loading 处理,提供完整实战代码与避坑指南。
dva 中怎么发起异步请求?effects 配合 call/put 实战
时间:2026-11-03
浏览:270
文章目录
在基于 React 的轻量级状态管理框架 dva 中,发起异步请求的核心方式是在 Model 的 effects 属性中定义 Generator 函数,配合 redux-saga 提供的两个核心辅助函数——call 与 put。其中,call 负责以声明式方式调用返回 Promise 的异步请求接口并等待结果,而 put 负责向当前或指定 Model 派发 action,触发 reducers 将请求到的数据同步写入 State。
一、核心机制:dva 为什么用 effects 处理异步?
在传统 React 应用中,如果直接在组件内调用接口并通过 setState 更新视图,一旦业务变得复杂,数据请求逻辑与 UI 渲染代码就会紧密耦合。当多个组件需要共享同一份异步数据,或需要集中处理加载状态、全局错误提示时,维护成本会迅速上升。
dva 借鉴并整合了 Redux、Redux-Saga 与 React-Router,将应用状态抽象为若干个独立的 Model。在 dva 的设计规范中,状态变更被严格划分为两套边界:
- reducers(同步纯函数):只负责根据旧状态和 action payload 计算返回全新的 State,函数内部绝对不能包含任何带有副作用(Side Effects)的操作,如网络请求、定时器或页面跳转。
- effects(异步副作用处理器):专门用来处理所有异步与不确定性操作。dva 底层基于 Redux-Saga,因此每一个 effect 都是一个 Generator(生成器)函数,使用
function*语法声明。
如图 1 所示,整个数据流遵循严格的单向闭环:当用户在组件中产生操作时,通过 dispatch 发送一个 action;如果该 action 对应的是 effects,dva 会将其交给 Redux-Saga 中间件。在 Generator 执行过程中,通过 yield call(...) 挂起函数并等待异步请求返回结果;获取数据后,再通过 yield put(...) 派发同步 action 给 reducers,最终改变 Store 中的 State,促使通过 @connect 绑定的视图组件重新渲染。
二、实战四步法:从接口定义到组件渲染
要完成一次完整的异步数据请求与渲染,在工程中推荐按以下四个步骤标准组织代码:定义 Service 层接口、配置 Model 的 effects 与 reducers、最后在组件中触发 dispatch。
步骤 1:在 services 目录封装异步请求
Service 层是网络 I/O 的唯一入口。它的职责仅仅是向后端发起 HTTP 请求并返回一个标准 Promise 对象,不包含任何与业务状态更新有关的代码。
// src/services/user.js
import request from '../utils/request';
/**
* 获取用户详细信息接口
* @param { id: string | number } params
* @returns {Promise} 返回解析后的响应数据
*/
export async function queryUserInfo(params) {
return request(`/api/users/${params.id}`, {
method: 'GET',
});
}
步骤 2:在 Model 中定义 effects 与 reducers
Model 是 dva 的核心载体。在 src/models/user.js 中,我们需要定义命名空间、初始状态、异步处理器 effects 和同步状态写入器 reducers。
// src/models/user.js
import { queryUserInfo } from '../services/user';
export default {
namespace: 'user',
state: {
userInfo: null,
lastUpdated: null,
},
effects: {
/**
* 异步获取用户信息
* @param { payload } action 传入参数
* @param { call, put, select } sagaEffects redux-saga 辅助函数
*/
*fetchUserInfo({ payload }, { call, put }) {
// 1. 使用 call 调用异步接口,并等待其 resolve
const response = yield call(queryUserInfo, payload);
// 2. 判断返回状态,将数据交由 reducer 同步写入 state
if (response && response.success) {
yield put({
type: 'saveUserInfo', // 同 Model 内可直接写 reducer 名称
payload: response.data,
});
}
},
},
reducers: {
// 纯函数:接收旧 state 和 action,返回全新 state
saveUserInfo(state, { payload }) {
return {
...state,
userInfo: payload,
lastUpdated: Date.now(),
};
},
},
};
在 effects 中,每个生成器函数的第二个参数提供了 dva 注入的 redux-saga 辅助函数包,包括 call、put、select、take 等。实战中最常使用的就是 call(调接口)与 put(发通知)。
步骤 3:在 React 组件中 dispatch 派发异步 Action
UI 组件通过 dva 提供的 connect 高阶组件连接 Store,拿到 dispatch 函数和需要的状态数据。在生命周期或用户交互中派发 action:
// src/routes/UserProfile.js
import React, { Component } from 'react';
import { connect } from 'dva';
class UserProfile extends Component {
componentDidMount() {
const { dispatch, userId } = this.props;
// 触发 user Model 中的 fetchUserInfo effect
dispatch({
type: 'user/fetchUserInfo',
payload: { id: userId || '1001' },
});
}
render() {
const { userInfo, loading } = this.props;
if (loading) {
return <div className="loading">正在加载用户数据...</div>;
}
if (!userInfo) {
return <div>暂无用户信息</div>;
}
return (
<div className="profile-card">
<h3>{userInfo.name}</h3>
<p>邮箱:{userInfo.email}</p>
<p>角色:{userInfo.role}</p>
</div>
);
}
}
// 订阅 Redux State
export default connect((state) => ({
userInfo: state.user.userInfo,
// 使用 dva-loading 插件自动提供的 loading 状态(详见第四节)
loading: state.loading.effects['user/fetchUserInfo'],
}))(UserProfile);
三、深度剖析:call 与 put 的核心区别与底层行为
虽然 call 和 put 都在 effects 函数中配合 yield 使用,但它们的本质行为和使用场景有着截然不同的分工。
| 维度 | call(fn, ...args) | put(action) |
|---|---|---|
| 核心角色 | 外部异步操作调用代理(I/O Invoker) | 内部状态变更事件派发器(Action Dispatcher) |
| 执行特征 | 阻塞式(Blocking):Saga 暂停生成器执行,直到 Promise 敲定(resolve/reject) | 同步非阻塞:派发 action 给 Redux Store,立即返回,代码继续向下走 |
| 目标对象 | 返回 Promise 的普通函数、API 接口服务、工具函数 | Model 内部的 reducers 或其他 effects |
| 返回值 | Promise resolve 的实际数据 | 通常无需接收返回值(派发结果对象) |
| 底层原理 | 返回形如 { type: 'CALL', fn, args } 的纯 JS 描述对象 |
返回形如 { type: 'PUT', action } 的纯 JS 描述对象 |
为什么必须声明式返回描述对象,而不是直接执行函数?
很多刚接触 Redux-Saga 的开发者会疑惑:为什么不直接写 const res = await queryUserInfo(payload),非要写 yield call(queryUserInfo, payload) 呢?
核心收益在于纯函数化与可测试性。当我们编写 call(queryUserInfo, payload) 时,它根本没有立即发起网络请求,而只是生成了一个普通的 JavaScript 对象:
{
'@@redux-saga/IO': true,
combinator: false,
type: 'CALL',
payload: {
fn: queryUserInfo,
args: [payload]
}
}
当测试用例运行 Generator 时,只需调用 generator.next().value,即可断言当前产出的对象是否与预期的调用签名一致,而无需在测试环境中侵入式 mock 浏览器底层的 fetch 或 axios 网络栈。具体的执行权被完整交给了外部的 Saga 运行时。
四、进阶体验:结合 dva-loading 自动化管理加载状态
在实际项目中,每一次异步请求往往都伴随着加载中(Loading)状态的切换。如果每个 Model 都要在 state 里手动声明 isLoading: false,在 effects 开头 yield put({ type: 'setLoading', payload: true }),结束时再手动设为 false,代码会变得冗长且极易遗漏。
dva 官方推荐使用 dva-loading 插件来自动化解决这一痛点。
1. 引入并注册 dva-loading
// src/index.js
import dva from 'dva';
import createLoading from 'dva-loading';
const app = dva();
// 注册 loading 插件
app.use(createLoading());
2. 在组件中精细化订阅 loading
注册插件后,dva 的全局 Redux State 中会自动注入一个名为 loading 的模型。它提供了多层级的加载标识:
state.loading.global:整个应用是否有任何异步 effect 正在执行(常用于全局进度条)。state.loading.models[namespace]:指定命名空间下是否有任意 effect 正在执行。state.loading.effects[`${namespace}/${effectName}`]:最常用,精确指向某一个具体异步请求的进行状态。
五、实战高频避坑与排查清单
在实际开发与线上排错中,关于 dva 异步请求的问题往往集中在以下四个方面:
1. 遗漏 yield 关键字导致执行失控
现象:调用 call 后,拿到的返回值是一个包含 type: 'CALL' 的纯对象而不是接口数据;或者调用 put 后,相关的 reducer 根本没有执行。
原因:Generator 函数内部只有遇到 yield 才会交出控制权。如果漏写了 yield,代码会将其当作普通的同步函数调用,生成了描述对象后直接丢弃,继续向下执行,导致整个异步调度失效。
2. call 传参方式写错(直接调用了函数)
错误代码:yield call(queryUserInfo(payload));
正确写法:yield call(queryUserInfo, payload);
解析:如果在 call(...) 内部直接写了括号 queryUserInfo(payload),该函数在代码运行到这一行时就会立刻被执行,Saga 中间件接收到的首个参数变成了该函数的返回值(Promise),而非函数引用。这破坏了 Saga 的托管机制,在复杂场景(例如重试、竞态取消)下会直接引发未知异常。
3. 跨 Model 调用时的 namespace 前缀遗漏
在同一个 Model 内部,yield put({ type: 'save', payload }) 中的 type 可以直接省略 namespace 前缀;但当需要在一个 Model 的 effect 中触发另一个 Model 的 reducer 或 effect 时,必须显式附带目标命名空间:
// 在 user Model 中通知全局消息提示组件
yield put({
type: 'global/showMessage', // 必须包含 'global/' 前缀
payload: { text: '用户信息更新成功!', type: 'success' },
});
4. 异步请求的异常捕获策略
如果在执行 yield call(...) 时后端接口返回了 HTTP 500、403 或发生断网超时,该 Promise 会进入 reject 状态。如果当前 effect 中没有进行捕获,整个 Generator 会抛出异常并中断后续代码。通常推荐双重兜底机制:
- 全局兜底:在
dva({ onError(err) { ... } })中配置全局统一错误捕获,弹窗通知网络异常并记录日志。 - 局部精准处理:对需要针对特定错误码进行业务降级的场景(如表单校验失败、密码错误),在 effect 中使用标准的
try...catch块包裹:
*fetchUserInfo({ payload }, { call, put }) {
try {
const res = yield call(queryUserInfo, payload);
yield put({ type: 'saveUserInfo', payload: res.data });
} catch (error) {
// 局部降级:写入错误状态,避免界面一直处于空白或卡死
yield put({ type: 'saveError', payload: error.message });
}
}
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。