阅读 1387

Redux-Saga 实用指北

本文适合对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的 forktake 构成的高阶函数,如果按官网的详细解释,可以写好几页了,这边主要记住这几点就够了!
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.typeGET_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-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


参考文档

关注下面的标签,发现更多相似文章
评论