导航
导航
文章目录󰁋
  1. 一、redux-thunk
    1. 1.1 redux 的副作用处理
    2. 1.2 redux-thunk 源码
    3. 1.3 redux-thunk 的缺点
  2. 二、redux-saga 简介
  3. 三、redux-saga 使用案例
  4. 四、redux-saga 使用细节
    1. 4.1 声明式的 Effect
    2. 4.2 Effect 提供的具体方法
      1. 4.2.1 take
      2. 4.2.2 call 和 apply
      3. 4.2.3 put
      4. 4.2.4 select
      5. 4.2.5 fork
      6. 4.2.6 takeEvery 和 takeLatest
  5. 五、案例分析一
    1. 5.1 LoginPanel 登录页
    2. 5.2 LoginSuccess 登录成功列表展示页
  6. 六、案例分析二
    1. 6.1 配置 saga 信息
    2. 6.2 配置reduce
    3. 6.3 处理action
    4. 6.4 处理sagas
  7. 七、总结
NEW
🚀

前端系统进阶指南

系统化学习前端知识

关注公众号

公众号:前端进阶之旅

浅析 redux-saga 中间件及用法,对比 redux-thunk

项目里第一次出现「点两下登录按钮发了两个请求」这种问题的时候,我用 redux-thunk 的写法是在 action 里加一个 loading 标记位挡住。挡是挡住了,但接着又来了「切页面时要取消上一个未完成的请求」「登录成功后自动拉一次列表,拉列表失败不影响登录状态」这类需求,标记位越加越多,一个 action 文件里塞满了 if (loading) return

那时候才意识到 thunk 解决的只是「redux 能不能接受异步」,没解决「异步流程怎么编排」。redux-saga 补的正是后面这块。这篇从 redux 为什么需要中间件讲起,拆 thunk 那十几行源码,再把 saga 的几个核心 Effect 逐个过一遍,最后配两个完整案例。文章写于 2018 年,末尾我另外补了一节现在的选型判断。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • redux 的数据流为什么装不下副作用,中间件插在哪个位置
  • redux-thunk 的完整源码,以及它为什么只有十几行
  • thunk 的局限:action 形式不统一、异步逻辑分散
  • redux-saga 的 watch / worker 工作模式和它的四个优点
  • takecallputselectforktakeEverytakeLatest 逐个拆
  • 阻塞调用和非阻塞调用的差别,以及它导致的真实体验问题
  • 一个登录登出加列表加载的完整案例,和一套工程化目录结构
  • thunk 和 saga 到今天该怎么选

一、redux-thunk

1.1 redux 的副作用处理

先看 redux 原本的数据流长什么样:

UI—————>action(plain)—————>reducer——————>state——————>UI

redux 原始数据流示意图,从 UI 到 action 到 reducer 再回到 UI

redux 是遵循函数式编程规则的。上述数据流中,action 是一个原始 js 对象(plain object),reducer 是一个纯函数。对于同步且没有副作用的操作,这条流水线足以管理数据、控制视图层更新。

问题出在「纯函数」这三个字上。发请求、读 localStorage、setTimeout 这些都是副作用,塞进 reducer 就破坏了纯度,reducer 一旦不纯,时间旅行调试和可预测性就全废了。

所以如果存在副作用函数,我们需要先处理副作用,然后生成原始的 js 对象。redux 的选择是在发出 actionreducer 处理函数之间插一层中间件来处理。

增加中间件之后的数据流大致如下:

UI——>action(side function)—>middleware—>action(plain)—>reducer—>state—>UI

redux 加入中间件处理副作用后的数据流示意图

在有副作用的 action 和原始的 action 之间增加中间件处理,从图里能看出中间件的职责只有一个:把异步操作转换掉,生成原始的 action。这样 reducer 函数就能处理相应的 action,从而改变 state、更新 UI

这个设计我觉得挺聪明的。它没有去改 reducer 的规则,而是在 action 到达 reducer 之前加了一道「提纯」工序,脏活全在中间件里干完,reducer 那边看到的永远是干净的普通对象。中间件机制本身是怎么用 compose 串起来的,我在 手写一个迷你版 Redux 里逐行实现过。

1.2 redux-thunk 源码

在 redux 中,thunk 是 redux 作者给出的中间件,实现极为简单,十几行代码:

function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}

return next(action);
};
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

这几行代码做的事情很简单:判别 action 的类型,如果 action 是函数,就调用这个函数,调用的形式为

action(dispatch, getState, extraArgument);

传进去的实参是 dispatchgetState,所以我们定义 thunk 形式的 action 时,形参一般就写这两个。如果 action 不是函数,就 next(action) 原样往下传给下一个中间件,跟没装过一样。

值得多看一眼的是这行嵌套箭头函数:({ dispatch, getState }) => next => action => {}。这就是 redux 中间件的标准签名,三层柯里化,第一层拿 store 的能力,第二层拿链条上的下一环,第三层才是真正处理 action 的地方。所有 redux 中间件都长这个样子,看懂这一个就都看懂了。

withExtraArgument 是给依赖注入用的。比如你想在所有 thunk 里都能直接拿到 axios 实例,就 applyMiddleware(thunk.withExtraArgument(api)),之后每个 thunk 的第三个参数就是它,省得到处 import。

1.3 redux-thunk 的缺点

thunk 的缺点也很明显。它仅仅做了「执行这个函数」这一件事,并不在乎函数主体内是什么。thunk 使得 redux 可以接受函数作为 action,但函数内部可以写成什么样完全没有约束。

比如下面是一个获取商品列表的异步操作所对应的 action:

export default ()=>(dispatch)=>{
fetch('/api/goodList',{ //fecth返回的是一个promise
method: 'get',
dataType: 'json',
}).then(function(json){
var json=JSON.parse(json);
if(json.msg==200){
dispatch({type:'init',data:json.data});
}
},function(error){
console.log(error);
});
};

从这个具有副作用的 action 里能看出,函数内部相当复杂:fetch 的配置、状态码判断、JSON 解析、错误处理全挤在一起。如果每一个异步操作都要这么定义一个 action,维护成本会飞快上涨。

具体不易维护的地方有两处。一是 action 的形式不统一,同步的是对象,异步的是函数,处理它们的心智模型完全不同。二是异步操作太分散,散落在各个 action 文件里,你想知道这个项目一共发了多少种请求,只能靠全局搜索。

这里还有个更细的问题:这段代码基本没法做单元测试。要测它就得 mock 掉 fetch,而 fetch 是在函数内部直接调用的,你没有插手的余地。

二、redux-saga 简介

redux-saga 是一个 redux 中间件,它有四个特性:集中处理 redux 副作用问题;被实现为 generator;属于类 redux-thunk 的中间件;采用 watch / worker(监听到执行)的工作形式。

最后那条是它和 thunk 结构上最大的差别。thunk 是「你 dispatch 一个函数,我帮你执行」,saga 是「你正常 dispatch 普通对象,我在旁边一直盯着,看到感兴趣的就自己动手」。UI 那边根本不知道 saga 存在。

redux-saga 的优点

  • 集中处理了所有的异步操作,异步接口部分一目了然
  • action 是普通对象,这跟 redux 同步的 action 一模一样
  • 通过 Effect,方便异步接口的测试
  • 通过 worker 和 watcher 可以实现非阻塞异步调用,同时还能实现非阻塞调用下的事件监听
  • 异步操作的流程是可以控制的,可以随时取消相应的异步操作

第三条我想多说两句,因为它是 saga 最被低估的价值。saga 里所有副作用都不是直接执行的,而是先 yield 一个描述对象出来,由中间件负责真正执行。测试的时候你只要检查「它 yield 出来的描述对象对不对」,完全不用 mock 网络层。这个设计是真的舒服。

基本用法

使用 createSagaMiddleware 方法创建 saga 的 Middleware,然后在创建 redux 的 store 时,用 applyMiddleware 函数把 saga Middleware 实例绑定到 store 上,最后调用 saga Middleware 的 run 函数来执行某个或者某些 saga。

在 saga 里,可以使用 takeEvery 或者 takeLatest 等 API 监听某个 action。当某个 action 触发后,saga 用 call 发起异步操作,操作完成后使用 put 函数触发新的 action,同步更新 state,从而完成整个 State 的更新。

三、redux-saga 使用案例

redux-saga 是受控执行的 generator。在 redux-saga 中 action 是原始的 js 对象,所有异步副作用操作都放在 saga 函数里面。这样既统一了 action 的形式,又让异步操作能被集中处理。

redux-saga 是通过 generator 实现的,如果运行环境不支持 generator,需要通过 babel-polyfill 转义。这一条在 2018 年还挺重要,那会儿要兼容的浏览器版本比现在低不少。

先从一个输出 hello saga 的最小例子开始。

创建一个 helloSaga.js 文件

export function * helloSaga() {
console.log('Hello Sagas!');
}

这个 generator 现在什么都没做,只是打印一句话,用来验证 saga 确实被中间件跑起来了。

在 redux 中使用 redux-saga 中间件

接下来在 main.js 里把中间件装上:

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { helloSaga } from './sagas'
const sagaMiddleware=createSagaMiddleware();
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(helloSaga);
//会输出Hello, Sagas!

和调用 redux 的其他中间件一样,想使用 redux-saga 中间件,只要在 applyMiddleware 中传入一个 createSagaMiddleware 的实例。唯一不同的是需要额外调用 run 方法,generator 才会开始执行。

这个 run 是必须的,忘了它就是新手最常见的「saga 一点反应都没有」。原因是 saga 不像别的中间件那样被动等 action,它需要主动启动一个根 generator 来铺开所有监听。

四、redux-saga 使用细节

4.1 声明式的 Effect

redux-saga 提供了一系列 API,比如 takeputallselect 等,这些 API 统称为 Effect。这些 Effect 执行后会返回一个描述对象,redux-saga 中间件根据这个描述对象去真正执行,执行完再恢复 generator 往下走。

关键点在于「描述」两个字。call(fetch, url) 这行代码并不发请求,它只是造了一个对象说「请帮我调用 fetch,参数是 url」。真正发请求的是中间件。

redux-thunk 的大体过程

action1(side function) -> redux-thunk 监听 -> 执行相应的有副作用的方法 -> action2(plain object)

redux-thunk 处理副作用的流程示意图

转化后的 action2 是一个原始 js 对象形式的 action,然后执行 reducer 函数就会更新 store 中的 state。

redux-saga 的大体过程

action1(plain object) -> redux-saga 监听 -> 执行相应的 Effect 方法 -> 返回描述对象 -> 恢复执行异步和副作用函数 -> action2(plain object)

redux-saga 通过 Effect 描述对象处理副作用的流程示意图

对比两张图能看出差别:redux-saga 监听到的是原始 js 对象 action,并且不会马上执行副作用操作,而是先通过 Effect 方法把它转化成一个描述对象,再拿这个描述对象作为标识,恢复执行副作用函数。

那多这一层描述对象到底图什么呢?

图的就是测试和可控。副作用不由你的代码直接触发,中间件就有机会在中间做取消、超时、竞态处理,测试时也可以只断言描述对象而不真的发请求。thunk 那边做不到这些,因为 fetch 已经在你的函数里被硬编码执行了。

4.2 Effect 提供的具体方法

下面介绍几个常用的 Effect,包括低阶 API(takecallapplyforkputselect)和高阶 API(takeEverytakeLatest)。

import { take, call, put, select, fork, takeEvery, takeLatest } from 'redux-saga/effects'

4.2.1 take

take 用来监听 action,返回的是监听到的 action 对象。比如有这么一个 action:

const loginAction = {
type: 'login'
}

UI Component 中 dispatch 一个 action:

dispatch(loginAction)

在 saga 中这样接:

const action = yield take('login');

它能监听到 UI 传递到中间件的 action,take 方法的返回值就是 dispatch 的那个原始对象。一旦监听到 login 动作,返回的 action 为:

{
type: 'login'
}

这里有个特点要留意:take 是一次性的。它拿到一个 action 之后就往下走了,想持续监听得自己套一个 while(true)。后面案例里满屏的 while(true) 就是这么来的,不是写错了。

4.2.2 call 和 apply

callapply 与 js 中的 callapply 相似,我们以 call 为例:

call(fn, ...args)

call 调用 fn,参数为 args,返回一个描述对象。这里传入的 fn 可以是普通函数,也可以是 generator。call 应用很广泛,redux-saga 里发异步请求基本都靠它:

yield call(fetch, '/userInfo', username)

要记住的一点是 call 会阻塞。写在它后面的语句必须等它 resolve 之后才能执行,这个特性在 5.2 节会引出一个真实的体验问题。

4.2.3 put

redux-saga 作为中间件,工作流是这样的:

UI——>action1————>redux-saga中间件————>action2————>reducer..

从工作流能看出,redux-saga 执行完副作用函数后必须发出 action,这个 action 被 reducer 监听到,才能达到更新 state 的目的。这里的 put 对应的就是 redux 中的 dispatch,流程图如下:

put 发出 action 到 reducer 的工作流程图

redux-saga 执行副作用方法转化 action 时,put 这个 Effect 跟 redux 原始的 dispatch 相似,都能发出 action,且发出的 action 都会被 reducer 监听到。用法是:

yield put({ type: 'login' })

差别在于 put 同样是声明式的,它返回描述对象,由中间件去 dispatch。所以在测试里你能直接断言「这个 saga 应该 put 出一个 type 为 login 的 action」。

4.2.4 select

put 与 redux 中的 dispatch 相对应,同样地,如果我们想在中间件里获取 state,那就需要 selectselect 对应的是 redux 中的 getState,用于获取 store 中的 state:

const id = yield select(state => state.id);

用它的时候有个坑:select 拿到的是调用那一刻的快照。如果你在 yield call(...) 之前 select 了一个值,等请求回来再用它,中间这段时间 state 可能已经被别的 action 改过了。需要最新值就在用之前重新 select 一次。

4.2.5 fork

fork 方法的效果类似 web worker 那种「另起一条线去跑」的感觉,它不会阻塞主线程,在非阻塞调用中十分有用。

需要说清楚的是它并不是真的开线程,JS 还是单线程的。fork 做的事是启动一个子任务然后立刻返回,不等它完成,后面的语句照常往下执行。所以它更准确的说法是「非阻塞地派生一个任务」。

和它配套的还有 cancel,可以主动取消一个 fork 出来的任务,这正是 thunk 完全做不到的那类能力。

4.2.6 takeEvery 和 takeLatest

takeEverytakeLatest 用于监听相应的动作并执行相应的方法,是构建在 takefork 上面的高阶 API。比如要监听 login 动作,用 takeEvery 可以这么写:

takeEvery('login', loginFunc)

takeEvery 监听到 login 动作就会执行 loginFunc 方法。它的特点是每一次触发都会派生一个新任务,多个同名 action 连着来,就会有多个 loginFunc 同时在跑。

takeLatest 的调用方式完全一样:

takeLatest('login', loginFunc)

takeEvery 不同的是,takeLatest 只保留最近一次被触发的那个任务,前面还没跑完的会被自动取消。

原文这里把两个名字写反了(写成了「与 takeLatest 不同的是,takeLatest 会…」),这次顺手改对。这两个的区别在实际项目里很关键:搜索框联想、按钮防重复提交这类场景用 takeLatest,因为你只关心最后一次的结果;而埋点上报、消息推送这种每条都不能丢的,必须用 takeEvery

选错的表现挺隐蔽的。用 takeEvery 做搜索联想,网络抖动的时候先发的请求后回来,输入框里显示的会是旧关键词的结果,用户看不出哪里不对,只觉得「搜出来的东西不对」。

五、案例分析一

接着我们来实现一个 redux-saga 样例:有一个登录页,登录成功后显示列表页,并且在列表页可以点击登出返回到登录页。

这个例子看着简单,但它把 saga 的几个关键点都覆盖到了,包括受控输入、请求、流程串联,以及最后那个阻塞与非阻塞的对比。

例子的最终展示效果如下:

登录页与登录成功后列表页的最终展示效果

样例的功能流程图为:

登录、拉列表、登出三条链路的功能流程图

对着流程图看代码会轻松很多。整条链路是 UI 发出大写的原始 action,saga 监听到之后执行副作用,再 put 出小写的 action 给 reducer。大写小写这个约定不是语法要求,是作者用来区分「UI 发出的意图」和「saga 处理完的结果」的一个习惯,实际项目里建议换成 USER/LOGIN_REQUESTUSER/LOGIN_SUCCESS 这种更明确的命名。

5.1 LoginPanel 登录页

实时保存用户名和密码

用户名输入框和密码框 onchange 时触发的函数为:

changeUsername:(e)=>{
dispatch({type:'CHANGE_USERNAME',value:e.target.value});
},
changePassword:(e)=>{
dispatch({type:'CHANGE_PASSWORD',value:e.target.value});
}

这两个函数最后会 dispatch 两个 action,CHANGE_USERNAMECHANGE_PASSWORD。注意它们都是普通对象,UI 层完全不知道 saga 的存在。

接着在 saga.js 文件中监听这两个动作并执行副作用函数,最后 put 发出转化后的 action,交给 reducer 处理:

function * watchUsername(){
while(true){
const action= yield take('CHANGE_USERNAME');
yield put({type:'change_username',
value:action.value});
}
}
function * watchPassword(){
while(true){
const action=yield take('CHANGE_PASSWORD');
yield put({type:'change_password',
value:action.value});
}
}

最后在 reducer 中接收到 redux-saga 的 put 方法传递过来的 action(change_usernamechange_password),然后更新 state。

这两个 watcher 里的 while(true) 就是前面说的,take 只取一次,想一直监听就得循环。看着像死循环,实际上 yield take(...) 会把 generator 挂起,直到对应的 action 来了才继续,不会占 CPU。

顺着上面聊,受控输入这个场景其实没必要走 saga 绕一圈,直接 reducer 处理就好。这里这么写纯粹是为了演示 takeput 的最小闭环。真项目里这么干,每敲一个字符都要过一遍 saga,属于给自己找麻烦。

监听登录事件判断登录是否成功

在 UI 中发出的登录事件为:

toLoginIn:(username,password)=>{
dispatch({type:'TO_LOGIN_IN',username,password});
}

登录事件的 action 是 TO_LOGIN_IN,对应的处理函数为:

while(true){
// 监听登入事件
const action1 = yield take('TO_LOGIN_IN');

const res = yield call(fetchSmart, '/login', {
method: 'POST',
body: JSON.stringify({
username: action1.username,
password: action1.password
})
});

if (res) {
yield put({ type: 'to_login_in' });
}
}

原文这段代码括号是对不上的,而且 put 前面漏了 yield,这次一并改对了。漏掉 yield 是个很典型的错误,因为 put(...) 只返回描述对象,不 yield 出去中间件就收不到,表现是「代码看着执行了但 state 没变」,非常难查。

处理函数的逻辑是:先监听原始动作,提取出传递来的用户名和密码,然后发请求判断是否登录成功,如果登录成功有返回值,就 putto_login_in 这个 action。

5.2 LoginSuccess 登录成功列表展示页

登录成功后的页面有两个功能:获取并展示列表信息,以及登出后返回登录页面。

获取列表信息

import {delay} from 'redux-saga';

function * getList(){
try {
yield delay(3000);
const res = yield call(fetchSmart,'/list',{
method:'POST',
body:JSON.stringify({})
});
yield put({type:'update_list',list:res.data.activityList});
} catch(error) {
yield put({type:'update_list_error', error});
}
}

这段有两个细节值得停一下。

一是 try / catch 包住了整个流程。saga 里的错误处理就是普通的 try catch,这比 thunk 那种到处 .catch() 直观得多,因为 generator 让异步代码长得跟同步一样。

二是那个 delay(3000)。为了演示请求过程,我们在本地 mock,通过 redux-saga 的工具函数 delay 延迟若干秒。真实请求本来就存在延迟,用 delay 可以在本地模拟出这种场景。

补一句版本相关的:delay 在 redux-saga 早期版本从 redux-saga 主包导出,从 v1 开始它被移到了 redux-saga/effects,也就是 import { delay } from 'redux-saga/effects'。老代码升级到 v1 之后如果报找不到 delay,去改这行 import 就行。

登出功能

const action2 = yield take('TO_LOGIN_OUT');
yield put({ type: 'to_login_out' });

与登入相似,登出功能从 UI 处接收 TO_LOGIN_OUT,然后转发 to_login_out

完整的实现登入登出和列表展示的代码

function * getList(){
try {
yield delay(3000);
const res = yield call(fetchSmart,'/list',{
method:'POST',
body:JSON.stringify({})
});
yield put({type:'update_list',list:res.data.activityList});
} catch(error) {
yield put({type:'update_list_error', error});
}
}

function * watchIsLogin(){
while(true){
//监听登入事件
const action1=yield take('TO_LOGIN_IN');

const res=yield call(fetchSmart,'/login',{
method:'POST',
body:JSON.stringify({
username:action1.username,
password:action1.password
})
});

//根据返回的状态码判断登陆是否成功
if(res.status===10000){
yield put({type:'to_login_in'});
//登陆成功后获取首页的活动列表
yield call(getList);
}

//监听登出事件
const action2=yield take('TO_LOGIN_OUT');
yield put({type:'to_login_out'});
}
}

这段把整条业务流程串成了一个 generator,读起来是一条直线:等登录、发请求、判断状态码、通知 reducer、拉列表、等登出。这就是 saga 最大的卖点,流程可以写成顺序代码,而不是散在几个回调里。

通过请求状态码判断登录是否成功,登录成功后用这行拉列表:

yield call(getList)

这里埋着一个真实的体验问题call 方法是会阻塞的。

具体表现有三点。在 call 调用结束之前,它后面的语句无法执行;如果 call(getList) 存在延迟,后面那句 const action2 = yield take('TO_LOGIN_OUT')call 返回结果之前根本不会被执行;结果就是延迟期间用户点的登出操作会被完全忽略。

call 阻塞期间点击登出无响应的效果演示

图里就是这个现象。列表还在转圈的三秒里,登出按钮点了没有任何反应,用户只会觉得页面卡死了。

无阻塞调用

把这行

yield call(getList)

改成

yield fork(getList)

fork 不会阻塞后续语句,getList 在后台跑,take('TO_LOGIN_OUT') 立刻就位。这时候在白屏期间点击登出,可以立刻响应并返回登录页面。

这个 callfork 的一行改动,是我认为整篇文章里最有价值的一处。它说明 saga 不只是「让异步能写进 redux」,而是把「这一步该不该等」这种流程控制决策显式地交到了你手里。thunk 里想实现同样的效果,你得自己维护一堆状态标记,还不一定做得对。

选择标准也简单:后面的语句依赖这一步的结果,用 call;不依赖、可以并行跑的,用 fork

六、案例分析二

上面那个例子是把 saga 写在一起讲逻辑,这一节看看真实项目里的目录结构怎么摆。

6.1 配置 saga 信息

先在 src/store/configureStore.js 里把中间件装好:

import { createStore, applyMiddleware, compose } from 'redux'
// import {createLogger } from 'redux-logger'
import createHistory from 'history/createBrowserHistory'
import createSagaMiddleware from 'redux-saga';
import { routerMiddleware } from 'react-router-redux'
import rootSaga from '../sagas'
import rootReducer from '../reducers/'

export const history = createHistory()

const middleware = routerMiddleware(history)

//创建saga middleware
const sagaMiddleware = createSagaMiddleware();


const configureStore = preloadedState => {
// 安装 Redux-DevTools Chrome 插件后可用 composeEnhancers()
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

const store = createStore(
rootReducer,
preloadedState,
composeEnhancers(
applyMiddleware(sagaMiddleware,middleware)
)
)
sagaMiddleware.run(rootSaga);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
const nextRootReducer = require('../reducers').default
store.replaceReducer(nextRootReducer)
})
}

return store
}


export default configureStore

6.2 配置reduce

// src/reducers/index.js
import {combineReducers} from 'redux'
import {routerReducer as routing} from 'react-router-redux'

const rootReducer = combineReducers({
routing,
poetry : require('./poetry').default
})

export default rootReducer
// src/reducers/poetry.js

import * as ActionTypes from '../actions'

export default (state = {
fetching:false,
error:false,
errMsg:'',
data:[]
},action) => {
if(action.type === ActionTypes.FETCH_POETRY_REQUEST){
return Object.assign({...state,fetching:true,errMsg:''})
}else if(action.type === ActionTypes.FETCH_POETRY_SUCCESS){
const data = action.payload.data
return Object.assign({...state,fetching:false,data,errMsg:''})
}else if(action.type === ActionTypes.FETCH_POETRY_FAILURE){
return Object.assign({...state,fetching:false,error:true,errMsg:action.payload.errMsg})
}
return state
}

6.3 处理action

// src/action/index.js

import { createAction } from 'redux-actions';

export const COMMON_FETCHING = 'COMMON_FETCHING'
export const COMMON_OVER = 'COMMON_OVER'
export const MSG_SHOW = 'MSG_SHOW'
export const MSG_INIT = 'MSG_INIT'
export const POP_LOGIN = 'POP_LOGIN'
export const initMsg = () => ({type : MSG_INIT})


// 相比用thunk多了一步 多了个action 来触发saga woker
export const FETCH_POETRY_REQUEST = 'FETCH_POETRY_REQUEST'
export const FETCH_POETRY_SUCCESS = 'FETCH_POETRY_SUCCESS'
export const FETCH_POETRY_FAILURE = 'FETCH_POETRY_FAILURE'
export const fetchPoetryRequest = createAction(FETCH_POETRY_REQUEST)
export const fetchPoetrySuccess = createAction(FETCH_POETRY_SUCCESS)
export const fetchPoetryFauilure= createAction(FETCH_POETRY_FAILURE)

6.4 处理sagas


// src/sagas/index.js

import { all } from 'redux-saga/effects'

export default function* rootSaga() {
yield all([
...require('./fetchPoetry').default
])
}

// src/fetchPoetry.js

import {put,take,call,fork,takeEvery,select} from 'redux-saga/effects'
import {delay} from 'redux-saga'
import * as api from '../api'
import * as actionTypes from '../actions/'

// saga worker 监听FETCH_POETRY_REQUEST动作触发执行相应操作
function* fetchPoetrySaga(){
// yield delay(100)
// ======== 写法一 =========
// yield takeEvery(actionTypes.FETCH_POETRY_REQUEST,function*(action){
// // 调用this.props.fetchPoetryRequest({user:'poetries',age:23}) 传参进来这里
// // 也可以通过这样获取state中的参数 const state = yield select()
// const {user,age} = action
// try{
// const data = yield call(api.get({
// url:'/mock/5b7fd63f719c7b7241f4e2fa/tangshi/tang-shi'
// }))
// yield put(actionTypes.fetchPoetrySuccess({data:data.data.data}))
// }catch(error){
// yield put(actionTypes.fetchPoetryFauilure({errMsg:error.message}))
// }

// })
// === 写法二====
while(true){
// 当dispatch({type:FETCH_POETRY_REQUEST})的时候被这里监听 执行对应的请求
const {user,age} = yield take(actionTypes.FETCH_POETRY_REQUEST)
try{
const data = yield call(api.get({
url:'/mock/5b7fd63f719c7b7241f4e2fa/tangshi/tang-shi'
}))
yield put(actionTypes.fetchPoetrySuccess({data:data.data.data}))
}catch(error){
yield put(actionTypes.fetchPoetryFauilure({errMsg:error.message}))
}
}

}


// 导出所有的saga
export default [
fork(fetchPoetrySaga)
]

完整代码例子 https://github.com/poetries/redux-saga-template

七、总结

redux-saga做为redux中间件的全部优点

  • 统一action的形式,在redux-saga中,从UIdispatchaction为原始对象
  • 集中处理异步等存在副作用的逻辑
  • 通过转化effects函数,可以方便进行单元测试
  • 完善和严谨的流程控制,可以较为清晰的控制复杂的逻辑
支持一下
扫一扫,支持poetries
  • 微信扫一扫
  • 支付宝扫一扫