dva 中怎么发起异步请求?effects 配合 call/put 实战

作者:北岛时间:2026-11-03 18:25:53浏览:271

本文详细讲解在 dva 状态管理框架中如何发起异步请求。通过剖析 Model 中的 effects 机制,结合 call 与 put 两个核心操作符,从 API 接口封装、effects 异步调用、reducers 同步更新到组件 dispatch 与 loading 处理,提供完整实战代码与避坑指南。

dva 中怎么发起异步请求?effects 配合 call/put 实战
Dva.js 实战 Redux-Saga 异步状态管理 阅读时间:约 8 分钟

在基于 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* 语法声明。
dva 中怎么发起异步请求?effects 配合 call/put 实战
图 1:dva 单向数据流与异步副作用 (Side Effects) 架构闭环全貌

如图 1 所示,整个数据流遵循严格的单向闭环:当用户在组件中产生操作时,通过 dispatch 发送一个 action;如果该 action 对应的是 effects,dva 会将其交给 Redux-Saga 中间件。在 Generator 执行过程中,通过 yield call(...) 挂起函数并等待异步请求返回结果;获取数据后,再通过 yield put(...) 派发同步 action 给 reducers,最终改变 Store 中的 State,促使通过 @connect 绑定的视图组件重新渲染。

二、实战四步法:从接口定义到组件渲染

要完成一次完整的异步数据请求与渲染,在工程中推荐按以下四个步骤标准组织代码:定义 Service 层接口、配置 Model 的 effects 与 reducers、最后在组件中触发 dispatch。

dva 中怎么发起异步请求?effects 配合 call/put 实战
图 2:dva 异步请求实战四步流转流程

步骤 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(),
      };
    },
  },
};
重点语法说明:Generator 的参数解构

在 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 使用,但它们的本质行为和使用场景有着截然不同的分工。

dva 中怎么发起异步请求?effects 配合 call/put 实战
图 3:Redux-Saga Effect 辅助函数深度对比:call vs put
维度 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 异步请求的问题往往集中在以下四个方面:

dva 中怎么发起异步请求?effects 配合 call/put 实战
图 4: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 });
  }
}
作者最新文章
相关文章
更多