react篇lesson5(redux-saga)知識點

machinist發表於2021-12-01
前面幾篇文章講了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整體流程,這裡簡單做個流程總結:

  1. 在createSagaMiddleware中初始化channel,並且獲取從redux的middleware中釋放出來的store的控制權;
  2. 用bind將runSaga函式重新賦值給sagaMiddleware.run 並追加store的控制權以及經過初始化的channel;
  3. 在runSaga中獲取遍歷器物件(iterator),並呼叫proc檔案處理遍歷器物件(iterator);
  4. proc主要負責執行遍歷器物件,並通過IO標記和effectRunnerMap具體確認當前遍歷器物件主要處理的effect是哪一種,並呼叫effectRunnerMap中對應的函式進行處理;
    個人覺得這個apply和bind也算是一種妙用吧!括弧笑

相關文章