前面幾篇文章講了redux react-redux 今天就來講講redu-sage,為什麼要單獨拿這個中介軟體來說呢?想必大家都知道,因為這個中介軟體很普遍,對於我們在redux或者react-redux中處理非同步請求以及副作用,簡單的非同步我們可以是用redux-thunk,也是可以完成,但是對於比較複雜的情況saga應付起來就比較容易,也不易發生回撥地獄!
概念
redux-saga
是一個用於管理應用程式 Side Effect(副作用,例如非同步獲取資料,訪問瀏覽器快取等)的 library,它的目標是讓副作用管理更容易,執行更高效,測試更簡單,在處理故障時更容易。
前置知識
中文文件:https://redux-saga-in-chinese...
英文文件:https://redux-saga.js.org/
學習saga是有前提條件的如果以下知識點還不太清楚,那學起來可能會比較吃力,建議先行學習;
基本屬性以及實現
effect
概念:在 redux-saga
的世界裡,Sagas 都用 Generator 函式實現。我們從 Generator 裡 yield 純 JavaScript 物件以表達 Saga 邏輯。 我們稱呼那些物件為 Effect
注意下述程式碼中Interface
代表介面的意思,payload
代表引數,大寫的英文代表指令;
call
阻塞呼叫saga,只有call呼叫的saga有結果返回以後程式碼才會繼續執行;
使用:
yield call(Interface, payload);
fork
非阻塞呼叫saga,無需等待fork呼叫的saga程式碼繼續執行;
yield fork(Interface, payload);
all
阻塞呼叫可同時呼叫多個saga,類似於promise.all;
yield all([
Interface(payload),
Interface1(payload1),
]);
take
take建立一個命令物件,告訴middleware等待redux dipatch匹配的某個pattern的action;
const action = yield take(PATTERN);
put
這個函式用於建立dispatchEffect,可以修改redux store中的狀態,其實就是redux中dispatch的封裝
yield put({type: ACTION, payload: payload});
以上幾個effect 原始碼實現起來比較簡單,其實就是進行一個簡單的標記,告訴後續的程式我這裡是什麼操作而已!就直接貼核心原理程式碼了。
import effectTypes from "./effectTypes";
import { IO } form "./symbols";
// 標記操作型別
const makeEffect = (type, payload) => ({ [IO]: IO, type, payload });
export function take(pattern) {
return makeEffect(effectTypes.TAKE, { pattern })
}
export function put(action) {
return makeEffect(effectTypes.PUT, { action })
}
// call的fn是一個promise
export function call(fn, ...arg) {
return makeEffect(effectTypes.CALL, { fn, arg })
}
// fork的fn是一個generator函式
export function fork(fn, ...arg) {
return makeEffect(effectTypes.FORK, { fn, arg })
}
// all的fns是一個promise組成的陣列
export function all(fns) {
return makeEffect(effectTypes.ALL, fns)
}
兩個標記常量的檔案這裡直接給原始碼的地址吧!
createSagaMiddleware
原始碼中處理createSagaMiddleware
這個邏輯的函式名叫sagaMiddlewareFactory
import { stdChannel } from './channel';
import runSaga from './runSaga';
export default function createSagaMiddleware() {
let boundRunSaga;
// 因為需要比對actiony和pattern,需要保證使用的是一個channel所以在這裡初始化一次channel即可
let channel = stdChannel()
// 根據redux 的middleware對於中介軟體的處理我們可以瞭解這裡熱入參是getStore, dispatch
// 並且返回一個next => action => next(action)的函式,不瞭解的小夥伴可以去翻看下我之前寫的redux的middleware的原始碼
function sagaMiddleware({ getStore, dispatch }) {
// 因為我們希望runSaga可以獲取到store的控制權,並且接收sagaMiddleware.run函式的引數,所以我們
// 在這裡用bind快取賦值給boundRunSaga,並將控制權函式傳入,因為不需要改變作用域所以第一個引數為null
boundRunSaga = runSaga.bind(null, { channel, getStore, dispatch })
return next => action => {
const result = next(action)
channel.put(action)
return result
}
}
sagaMiddleware.run = (...args) => boundRunSaga(...args)
return sagaMiddleware
}
runSaga
import proc from "./proc"
export default function runSaga({ channel, getStore, disparch }, saga, ...args) {
// 這個saga就是generator方法,我們需要執行才能獲取到遍歷器物件
// 我們需要拿到遍歷器物件才能拿到裡面的狀態,執行裡面的effect
// 這步驟我們需要我們替使用者操作
const iterator = saga(args)
// 根據generator惰性求值的特點,我們單獨宣告一個檔案(proc)去處理generator的next方法
// proc需要處理的是遍歷器物件,以及過程中需要修改狀態所以需要{ getStore, disparch }, iterator作為引數
const env = { channel, getStore, disparch }
proc(env, iterator)
}
proc
功能:接受runSaga傳遞過來的遍歷器物件,呼叫遍歷器物件的next函式,並且以及effect的標記呼叫effectRunnerMap中對應的函式
import effectRunnerMap from "./effectRunnerMap";
import { IO } form "./symbols";
export default function proc(env, iterator, cb) {
// 這裡面我們需要處理next函式,所以我們需要自己定義下next
// 首次呼叫是不需要引數的
next();
function next(arg, isErr) {
let result;
// 執行中我們需要判斷是否存在錯誤,確定無錯誤的時候才正常執行遍歷器物件的next函式
if (isErr) {
// 在這裡的arg是具體的錯誤資訊
result = iterator.throw(arg)
}
else {
result.next(arg)
}
// result {value, done: true/false}
// 如果done為fasle,說明遍歷未結束,需要繼續遍歷
if (!result.done) {
digesEffect(result.value, next)
}
else {
// 遍歷結束
if (cb && typeof cb === "function") {
cb(result)
}
}
}
function runEffect(effect, currCb) {
// 判斷這裡的effect方法是不是saga內部定義的
if (effect && effect[IO]) {
// 根據標記獲取對應的方法
const effectRunner = effectRunnerMap[effect.type]
effectRunner(env, effect.payload, currCb)
}
else {
// 如果不是內部定義的effect,則直接執行currCb,進行下一次next
currCb()
}
}
// 我們需要在digesEffect在處理具體的effect比如take/put/call等等
function digesEffect(effect, cb) {
// 在這裡我們需要判斷一下effect的執行狀態如果執行結束就不需要重複執行
let effectSettled;
function currCb(res, isErr) {
if (effectSettled) {
return
}
effectSettled = true
cb(res, isErr)
}
runEffect(effect, currCb)
}
}
effectRunnerMap
功能:這裡存放的是take、call等副作用的具體處理邏輯包括修改store中state的操作
import effectTypes from './effectTypes'
import proc from "./proc"
import { promise, iterator } from './is'
// 這個檔案主要是和effect方法中的標記相對應根據當時標記獲取這裡對應的方法
// channel 這樣獲取是因為原始碼中的take是可以接受外界傳進來的channel的,預設使用env當中的
function runTakeEffect(env, { channel = env.channel, pattern }, cb) {
// 我們只有發起一次dispatch拿到對應的pattern
//並且pattern和dispatch的action匹配上才會去執行cb
const matcher = input => input.type === pattern;
// 匹配以後我們需要把cb 和 pattern關聯以後儲存起來等待dispatch之後呼叫
// 所以我們宣告一個channel來儲存
channel.take(cb, matcher)
}
function runPutEffect(env, { action }, cb) {
// put 其實就是修改store中的state的過程,所以直接執行dispatch就可以了,
// 同樣的我們執行只有繼續呼叫cb,並把dispatch的執行結果返回
const result = env.dispatch(action)
cb(result)
}
function runCallEffect(env, { fn, args }, cb) {
// call這裡的fn可能是promise,也可能是generator函式,也可能就是普通函式需要區分
// 原始碼中專門判斷返回的result的型別是不是promise型別,是一個叫is的靜態檔案
const result = fn.apply(null, args)
// 原始碼中是呼叫的resolvePromise函式來判斷的,在resolvePromise中引用了is檔案
if (promise(result)) {
// 在then中回撥cb
result.then(resp => cb(resp)).catch(error => cb(error, true))
return
}
// iterator也是從is靜態檔案取出來的
if (iterator(result)) {
// 在proc函式上加一個新的引數,目的是在遍歷器結果done為true的時候才去執行cb從而達到阻塞的效果
proc(env, result, cb)
return
}
// 如果是普通函式的我們直接呼叫cb
cb(result)
}
function runForkEffect(env, { fn, args }, cb) {
// 先執行fn, fn是generator函式,執行fn先拿到遍歷器物件,然後在執行遍歷器物件的next
// 所以我們繼續交給proc來處理就好了
// 這裡需要注意的是啊這個apply,我們之前標記fork函式的時候對args進行了解構,所以這裡的args是一個類陣列物件
// 而使用者呼叫fork的是傳入的第二個引數是payload,所以這裡我們其實應該寫fn(args[0])才能獲取到正確的payload,
// 但是為了更好的相容,原始碼中使用了fn.apply(args),利用apply接受一個類陣列引數的原理,對引數進行解構
const iterator = fn.apply(args)
proc(env, iterator)
// 處理完成完以後,直接呼叫cb即可,因為fork是非阻塞的
cb()
}
function runAllEffect(env, fns, cb) {
// 這裡的fns是遍歷器物件組成的陣列,我們遍歷這個陣列就可以拿到每一個遍歷器物件
// 然後繼續使用proc檔案處理這個遍歷器物件
const len = fns.length;
for (let i = 0; i < len; i++) {
proc(env, fns[i])
}
}
const effectRunnerMap = {
[effectTypes.TAKE]: runTakeEffect,
[effectTypes.PUT]: runPutEffect,
[effectTypes.CALL]: runCallEffect,
[effectTypes.FORK]: runForkEffect,
[effectTypes.ALL]: runAllEffect,
}
export default effectRunnerMap
channel
需要在createSagaMiddleware中初始化
我們使用take和put來與redux store進行通訊,channel概括了這些effect與外部事件源或sagas之間的通訊;
import { MATCH } form "./symbols";
export function stdChannel() {
// 宣告一個變數來儲存,因為有可能是多個所以使用陣列
let currentTakers = [];
function take(cb, matcher) {
cb[MATCH] = matcher
currentTakers.push(cb)
}
function put(input) {
const takers = currentTakers;
// 因為currentTakers是動態變化的如果這裡不賦值給len有可能會造成死迴圈
for (let i = 0, len = takers.length; i < len; i++) {
const taker = takers[i];
if (taker[MATCH](input)) {
taker(input)
}
}
}
return {
take, put
}
}
總結
以上就是一些基礎的effect的核心邏輯程式碼,以及saga整體流程,這裡簡單做個流程總結:
- 在createSagaMiddleware中初始化channel,並且獲取從redux的middleware中釋放出來的store的控制權;
- 用bind將runSaga函式重新賦值給sagaMiddleware.run 並追加store的控制權以及經過初始化的channel;
- 在runSaga中獲取遍歷器物件(iterator),並呼叫proc檔案處理遍歷器物件(iterator);
- proc主要負責執行遍歷器物件,並通過IO標記和effectRunnerMap具體確認當前遍歷器物件主要處理的effect是哪一種,並呼叫effectRunnerMap中對應的函式進行處理;
個人覺得這個apply和bind也算是一種妙用吧!括弧笑