Redux-Saga 實用指北

yellowfrogCN發表於2018-04-19

本文適合對Redux有一定了解,或者重度失眠患者閱讀!

前言

  • 本文需求:利用Redux-Saga,向 GitHub 獲取Redux作者 Dan Abramov 的資料,渲染頁面;但是,在非同步獲取GitHub資料的時候,可以點選取消按鈕/或者請求時間超過5000ms時,取消這個非同步請求;
  • 現有環境:自行搭建環境還是比較繁瑣的,可以直接去我GitHub地址clone下來: redux-saga-example,別忘了install

因為本文主要講Redux-Saga,故action、view、reducer這塊就快速掠過;第一步傳送一個action,好讓Saga那邊監聽到!


Redux-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 實用指北

加入手動取消

剛才是最正常的情況下走了一遍Redux-Saga,那假如產品在這個基礎上,提了要求:再正在請求的Dan資料的時候,可以手動取消這個非同步請求呢? 相信這需求對於前端的小夥伴來說,還是比較難的吧!


Redux-Saga 實用指北


  • 如何實現需求呢?

還記得剛才對fork解釋的三點嗎?其中有第三點就介紹了fork是可以取消的。
剛才是說rootSaga裡的takeLatest負責監聽,getDataSaga負責執行,那要想控制這個執行函式,則要在這兩個函式中間再插入一個函式,就變成了takeLatest監聽到GET_DATA_REQUEST後,去執行這個控制函式,直接看程式碼


Redux-Saga 實用指北


為了更加方便的檢視效果,我們手動加入延遲

import { delay } from 'redux-saga';
...
try {
    ...
    yield delay(2000); // 手動延遲2秒
    ...
}
...
複製程式碼

這是點選確定按鈕,在請求的過程中,點選取消按鈕,就發現這個非同步被取消了!!完美解決!!!
這裡要輕噴一下,Redux-Saga官網推薦的Redux-Saga中文文件,裡面有錯誤的地方,也沒修正;同樣來自Redux-Saga官網推薦Redux-Saga繁體文件就沒問題!- -!!!


Redux-Saga 實用指北


加入超時自動取消

這時候,加入產品在以上基礎上,再次提了要求:不光可以手動取消這個非同步請求,還要加入超時自動取消這個非同步請求,超時時間為5秒呢? 這讓我想到了上古時代的AJAX, 那時候封裝好的AJAX都是會有個timeOut 預設5秒給我們,超過了這個timeOut,就會自動取消非同步請求

  • 題外話:現在一個在Vue.Js中大紅大紫的非同步外掛Axios有這個功能!而這裡的演示是完全利用Redux-Saga這個強大到變態的功能來解決超時自動取消的問題的,沒使用Axios ......- -!

Redux-Saga 實用指北


  • 那,利用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觀看非同步請求,可以更清晰直觀

Redux-Saga 實用指北



Redux-Saga 實用指北


裝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

Redux-Saga 實用指北


參考文件

相關文章