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


參考文檔

相關文章
相關標籤/搜索