本文适合对Redux有一定了解,或者重度失眠患者阅读!
前言
- 本文需求:利用Redux-Saga,向 GitHub 获取Redux作者 Dan Abramov 的数据,渲染页面;但是,在异步获取GitHub数据的时候,可以点击取消按钮/或者请求时间超过5000ms时,取消这个异步请求;
- 现有环境:自行搭建环境还是比较繁琐的,可以直接去我GitHub地址clone下来: redux-saga-example,别忘了install
因为本文主要讲Redux-Saga,故action、view、reducer这块就快速掠过;第一步发送一个action,好让Saga那边监听到!
监听函数:takeLatest与takeEvery
来到Saga这边,直接上代码!!
// homeSaga.js
import {
takeLatest, // 短时间内(没有执行完函数)多次触发的情况下,指会触发相应的函数一次
// takeEvery, // takeLatest 的同胞兄弟,不同点是每次都会触发相应的函数
put, // 作用跟 dispatch 一毛一样,可以就理解为dispatch
call // fork 的同胞兄弟,不过fork是非阻塞,call是阻塞,阻塞的意思就是到这块就停下来了
} from 'redux-saga/effects';
import * as actions from '../actions/homeAction';
import fetch from '../utils/fetch';
export default function* rootSaga () {
yield takeLatest('GET_DATA_REQUEST', getDataSaga); // 就在这个rootSaga里面利用takeLatest去监听action的type
}
function* getDataSaga(action) {
try {
yield put(actions.requestDataAction(true, 'LOADING')); // 开启loading
const userName = action.payload;
// 1、也可以这么写: const result = yield fetch(url地址, params);
// 2、这边用 call 是为了以后需要的 saga 测试
// https://api.github.com/users/userName 是github的个人信息
const url = `https://api.github.com/users/${userName}`;
const api = (params) => fetch(url, params);
const result = yield call(api);
if (result) {
// 成功后:即将在 reducer 里做你想做的事情
yield put(actions.requestDataAction(result, 'SUCCESS'));
}
} catch (e) {
// 失败后:即将在 reducer 里做你想做的事情
yield put(actions.requestDataAction(e, 'ERROR'));
} finally {
// 不管成功还是失败还是取消等,都会经过这里
yield put(actions.requestDataAction(false, 'LOADING')); // 关闭loading
yield put(actions.requestDataAction(null, 'FINISH')); // 打印一个结束的action,一般没什么用
}
}
复制代码
rootSaga
可以理解为是一个监听函数,在创建store中间件的时候就已经执行了;rootSaga
里面通过引入的 takeLatest
去去监听刚才的的action.type: 'GET_DATA_REQUEST', 我们去看下takeLatest
的源码
const takeLatest = (patternOrChannel, saga, ...args) => fork(function*() {
let lastTask
while (true) {
const action = yield take(patternOrChannel)
if (lastTask) {
yield cancel(lastTask) // cancel is no-op if the task has already terminated
}
lastTask = yield fork(saga, ...args.concat(action))
}
})
复制代码
通过源码的看出来,这个takeLatest
是也是由redux-saga的 fork 与 take 构成的高阶函数,如果按官网的详细解释,可以写好几页了,这边主要记住这几点就够了! fork
:
- 1、
fork
是非阻塞的,非阻塞就是遇到它,不需要等它执行完, 就可以直接往下运行; - 2、
fork
的另外一个同胞兄弟call是阻塞,阻塞的意思就是一定要等它执行完, 才可以直接往下运行; - 3、fork是返回一个任务,这个任务是可以被取消的;而call就是它执行的正常返回结果!(非常重要)
take
: take
是阻塞的,主要用来监听action.type
的,只有监听到了,才会继续往下执行;
从上面的解释,会有点跟我们的对程序运行的认知不太一样,因为当这个 takeLatest
高阶函数运行到
const action = yield take(patternOrChannel)
复制代码
这一段时,这个函数就停在这里了,只有当这个take
监听到action.type
的时候,才会继续往下执行;
所以,rootSaga
函数执行的时候,yield takeLatest('GET_DATA_REQUEST', getDataSaga);
也执行了,也就是运行到const action = yield take(patternOrChannel)
这步停下来,监听以后发出的 GET_DATA_REQUEST
;当我们点击按钮发出这个type为GET_DATA_REQUEST
的action,那么这个take
就监听到,从而就继续往下运行
if (lastTask) {
yield cancel(lastTask)
}
复制代码
这一段的意思就是区别takeLatest与它的同胞兄弟takeEvery的区别,takeLatest
是在他的程序没运行完时,再次运行时,会取消它的上一个任务;而takeEvery
则是运行都会fork一个新的任务出来,不会取消上一个任务;所以,用takeLatest
来处理重复点击的问题,无敌好用!
lastTask = yield fork(saga, ...args.concat(action))
复制代码
最后这句就是运行takeLatest
里的函数,通过ES6的REST语法
,传相应的参数过去,如果在takeLatest
里面没有传第三个及以上的参数,那么就只传这个take
监听到的action
过去;
所以所以,对rootSaga
函数里面这个 yield takeLatest('GET_DATA_REQUEST', getDataSaga)
说了那么多,可以理解为就是一句话,监听action.type
为GET_DATA_REQUEST
的action,并运行getDataSaga(action)
这个函数;
对了,只要是Generator函数
,要加 * 号啊!!
程序运行到getDataSaga
这个函数,推荐写法是加入try catch
写法
try {
// 主要程序操作点
} catch(e) {
// 捕捉到错误后,才会运行的地方
} finally {
// 任何情况下都会走到这里,如果非必要的情况下,可以省略 finally
}
复制代码
具体每一步的作用都用注释的方式写出来了,还是比较直观的!这里再对一些生面孔说明一下,
put
:你就认为put
就等于dispatch
就可以了;call
:刚才已经解释过了,阻塞型的,只有运行完后面的函数,才会继续往下;在这里可以片面的理解为promise
里的then
吧!但写法直观多了!
好了,里面的每个put(action)
就相当dispatch(action)
出去,reducer
边接收到相应的action.type
就会对数据进行相应的操作,最后通过react-redux
的connect
回到视图中,完成了一次数据驱动视图,也是什么所谓的MVVM
- 成功后返回 Redux作者 Dan Abramov 的个人信息,好帅啊··············
加入手动取消
刚才是最正常的情况下走了一遍Redux-Saga
,那假如产品在这个基础上,提了要求:再正在请求的Dan数据的时候,可以手动取消这个异步请求呢? 相信这需求对于前端的小伙伴来说,还是比较难的吧!
- 如何实现需求呢?
还记得刚才对fork
解释的三点吗?其中有第三点就介绍了fork
是可以取消的。
刚才是说rootSaga
里的takeLatest
负责监听,getDataSaga
负责执行,那要想控制这个执行函数,则要在这两个函数中间再插入一个函数,就变成了takeLatest
监听到GET_DATA_REQUEST
后,去执行这个控制函数,直接看代码
为了更加方便的查看效果,我们手动加入延迟
import { delay } from 'redux-saga';
...
try {
...
yield delay(2000); // 手动延迟2秒
...
}
...
复制代码
这是点击确定按钮,在请求的过程中,点击取消按钮,就发现这个异步被取消了!!完美解决!!!
这里要轻喷一下,Redux-Saga
官网推荐的Redux-Saga中文文档,里面有错误的地方,也没修正;同样来自Redux-Saga
官网推荐Redux-Saga繁体文档就没问题!- -!!!
加入超时自动取消
这时候,加入产品在以上基础上,再次提了要求:不光可以手动取消这个异步请求,还要加入超时自动取消这个异步请求,超时时间为5秒呢? 这让我想到了上古时代的AJAX
, 那时候封装好的AJAX
都是会有个timeOut 默认5秒给我们,超过了这个timeOut,就会自动取消异步请求
- 题外话:现在一个在
Vue.Js
中大红大紫的异步插件Axios
有这个功能!而这里的演示是完全利用Redux-Saga
这个强大到变态的功能来解决超时自动取消的问题的,没使用Axios
......- -!
- 那,利用Redux-Saga又如何实现这个需求呢?
答案就是Redux-Saga
自带的race,用一句话解释就是,队列里面,谁先了就用谁,抛弃其他!骚微改造一下controlSaga
这个函数
function* controlSaga (action) {
const task = yield fork(getDataSaga, action); // 运行getDataSaga这个任务
yield race([ // 利用rece谁先来用谁的原则,完美解决 超时自动取消与手动取消的 的问题
take('CANCEL_REQUEST'), // 到这步时就阻塞住了,直到发出type为'CANCEL_REQUEST'的action,才会继续往下
call(delay, 1000) // 控制时间
]);
yield cancel(task); // 取消任务,取消后,cancelled() 会返回true,否则返回false
}
复制代码
因为我们刚才在try{...}
里面加入了yield delay(2000)
延时两秒,为了保证超时间一定快过异步请求时间,这边的超时时间我们用1秒。然后点击确认按钮,在什么都不做的情况下,就可以看到请求一下后,自动就取消了!完美...(一般默认的timeOut为5秒)
- 到这里,已经完美解决了一开始时提的需求:加入超时自动取消与手动取消的功能;
- 打开
F12
观看异步请求,可以更清晰直观
装X之路:封装这个controlSaga,方便(wan)别(mei)人(zhuang)使(bility)用
- 之前我们已经看过takeLatest的源码,利用高阶函数,来封装一个通用的
controlSaga
// controlSaga.js
import { take, fork, race, call, cancel, put } from 'redux-saga/effects';
import { delay } from 'redux-saga';
// 普通函数,故不需要加 *
function controlSaga (fn) {
// 返回一个 Generator函数
/**
* @param timeOut: 超时时间, 单位 ms, 默认 5000ms
* @param cancelType: 取消任务的action.type
* @param showInfo: 打印信息 默认不打印
*/
return function* (...args) {
// 这边思考了一下,还是单单传action过去吧,不想传args这个数组过去, 感觉没什么意义
const task = yield fork(fn, args[args.length - 1]);
const timeOut = args[0].timeOut || 5000; // 默认5秒
// 如果真的使用这个controlSaga函数的话,一般都会传取消的type过来, 假如真的不传的话,配合Match.random()也能避免误伤
const cancelType = args[0].cancelType || `NOT_CANCEL${Math.random()}`;
const showInfo = args[0].showInfo; // 没什么用,打印信息而已
const result = yield race({
timeOut: call(delay, timeOut),
// 实际业务需求
handleToCancel: take(cancelType)
});
if (showInfo) {
if (result.timeOut) yield put({type: `超过规定时间${timeOut}ms后自动取消`})
if (result.handleToCancel) yield put({type: `手动取消,action.type为${cancelType}`})
}
yield cancel(task);
}
}
export default controlSaga;
复制代码
- 然后引用这个封装好的controlSaga,如下图,
takeLatest
第二个参数是用controlSaga(fn)
包裹住,然后通过第三个参数往controlSaga
里面传控制参数即可,超方便供人使用的- -.V