Redux-Saga 实用指北

本文适合对Redux有必定了解,或者重度失眠患者阅读!html

前言

  • 本文需求:利用Redux-Saga,向 GitHub 获取Redux做者 Dan Abramov 的数据,渲染页面;可是,在异步获取GitHub数据的时候,能够点击取消按钮/或者请求时间超过5000ms时,取消这个异步请求;
  • 现有环境:自行搭建环境仍是比较繁琐的,能够直接去我GitHub地址clone下来: redux-saga-example,别忘了install

由于本文主要讲Redux-Saga,故action、view、reducer这块就快速掠过;第一步发送一个action,好让Saga那边监听到!前端



监听函数:takeLatest与takeEvery

来到Saga这边,直接上代码!!react

// 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;
        // 一、也能够这么写: const result = yield fetch(url地址, params);
        // 二、这边用 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 的源码ios

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的 forktake 构成的高阶函数,若是按官网的详细解释,能够写好几页了,这边主要记住这几点就够了!
fork:git

  • 一、fork是非阻塞的,非阻塞就是遇到它,不须要等它执行完, 就能够直接往下运行;
  • 二、fork的另一个同胞兄弟call是阻塞,阻塞的意思就是必定要等它执行完, 才能够直接往下运行;
  • 三、fork是返回一个任务,这个任务是能够被取消的;而call就是它执行的正常返回结果!(很是重要)

    take:
  • take是阻塞的,主要用来监听action.type的,只有监听到了,才会继续往下执行;

从上面的解释,会有点跟咱们的对程序运行的认知不太同样,由于当这个 takeLatest 高阶函数运行到github

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就监听到,从而就继续往下运行redux

if (lastTask) {
      yield cancel(lastTask)
    }
复制代码

这一段的意思就是区别takeLatest与它的同胞兄弟takeEvery的区别,takeLatest是在他的程序没运行完时,再次运行时,会取消它的上一个任务;而takeEvery则是运行都会fork一个新的任务出来,不会取消上一个任务;因此,takeLatest来处理重复点击的问题,无敌好用!api

lastTask = yield fork(saga, ...args.concat(action))
复制代码

最后这句就是运行takeLatest里的函数,经过ES6的REST语法,传相应的参数过去,若是在takeLatest里面没有传第三个及以上的参数,那么就只传这个take监听到的action过去;
因此因此,对rootSaga函数里面这个 yield takeLatest('GET_DATA_REQUEST', getDataSaga)说了那么多,能够理解为就是一句话,监听action.typeGET_DATA_REQUEST的action,并运行getDataSaga(action)这个函数;
对了,只要是Generator函数,要加 * 号啊!!
数组

程序运行到getDataSaga这个函数,推荐写法是加入try catch写法promise

try {
    // 主要程序操做点
} catch(e) {
    // 捕捉到错误后,才会运行的地方
} finally {
    // 任何状况下都会走到这里,若是非必要的状况下,能够省略 finally
}
复制代码

具体每一步的做用都用注释的方式写出来了,仍是比较直观的!这里再对一些生面孔说明一下,

  • put:你就认为put就等于 dispatch就能够了;
  • call:刚才已经解释过了,阻塞型的,只有运行完后面的函数,才会继续往下;在这里能够片面的理解为promise里的then吧!但写法直观多了!

好了,里面的每一个put(action)就至关dispatch(action)出去,reducer边接收到相应的action.type就会对数据进行相应的操做,最后经过react-reduxconnect回到视图中,完成了一次数据驱动视图,也是什么所谓的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


参考文档

相关文章
相关标签/搜索