本文適合對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