redux-saga 初識

原文連接html

redux-saga 是一個管理 Redux 應用異步操做的中間件,功能相似redux-thunk + async/await, 它經過建立 Sagas 將全部的異步操做邏輯存放在一個地方進行集中處理。ios

redux-saga 的 effects

redux-saga中的 Effects 是一個純文本 JavaScript 對象,包含一些將被 saga middleware 執行的指令。這些指令所執行的操做包括以下三種:git

  • 發起一個異步調用(如發一塊兒一個 Ajax 請求)
  • 發起其餘的 action 從而更新 Store
  • 調用其餘的 Sagas

Effects 中包含的指令有不少,具體能夠異步API 參考進行查閱github

redux-saga 的特色

  • 方便測試,例如:
assert.deepEqual(iterator.next().value, call(Api.fetch, '/products'))
  • action 能夠保持其純淨性,異步操做集中在 saga 中進行處理
  • watch/worker(監聽->執行) 的工做形式
  • 被實現爲 generator
  • 對含有複雜異步邏輯的應用場景支持良好
  • 更細粒度地實現異步邏輯,從而使流程更加清晰明瞭,遇到 bug 易於追蹤和解決。
  • 以同步的方式書寫異步邏輯,更符合人的思惟邏輯

從 redux-thunk 到 redux-saga

假如如今有一個場景:用戶在登陸的時候須要驗證用戶的 username 和 password 是否符合要求。redux

使用 redux-thunk 實現

獲取用戶數據的邏輯(user.js):axios

// user.js

import request from 'axios';

// define constants
// define initial state
// export default reducer

export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}

驗證登陸的邏輯(login.js):swift

import request from 'axios';
import { loadUserData } from './user';

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

redux-saga

異步邏輯能夠所有寫進 saga.js 中:api

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST) //等待 Store 上指定的 action LOGIN_REQUEST
    try {
      let { data } = yield call(loginRequest, { user, pass }); //阻塞,請求後臺數據
      yield fork(loadUserData, data.uid); //非阻塞執行loadUserData
      yield put({ type: LOGIN_SUCCESS, data }); //發起一個action,相似於dispatch
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(userRequest, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

難點解讀

對於 redux-saga, 仍是有不少比較難以理解和晦澀的地方,下面筆者針對本身以爲比較容易混淆的概念進行整理:數組

take 的使用

take 和 takeEvery 都是監聽某個 action, 可是二者的做用卻不一致,takeEvery 是每次 action 觸發的時候都響應,而 take 則是執行流執行到 take 語句時才響應。takeEvery 只是監聽 action, 並執行相對應的處理函數,對什麼時候執行 action 以及如何響應 action 並無多大的控制權,被調用的任務沒法控制什麼時候被調用,而且它們也沒法控制什麼時候中止監聽,它只能在每次 action 被匹配時一遍又一遍地被調用。可是 take 能夠在 generator 函數中決定什麼時候響應一個 action 以及 響應後的後續操做。
例如在監聽全部類型的 action 觸發時進行 logger 操做,使用 takeEvery 實現以下:app

import { takeEvery } from 'redux-saga'

function* watchAndLog(getState) {
  yield* takeEvery('*', function* logger(action) {
      //do some logger operation //在回調函數體內
  })
}

使用 take 實現以下:

import { take } from 'redux-saga/effects'

function* watchAndLog(getState) {
  while(true) {
    const action = yield take('*')
    //do some logger operation //與 take 並行 
  })
}

其中 while(true) 的意思是一旦到達流程最後一步(logger),經過等待一個新的任意的 action 來啓動一個新的迭代(logger 流程)。

阻塞和非阻塞

call 操做是用來發起異步操做的,對於 generator 來講,call 是阻塞的操做,它在 Generator 調用結束以前不能執行或處理任何其餘事情。,可是 fork 倒是非阻塞操做,當 fork 調動任務時,該任務會在後臺執行,此時的執行流能夠繼續日後面執行而不用等待結果返回。

例如以下的登陸場景:

function* loginFlow() {
  while(true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    const token = yield call(authorize, user, password)
    if(token) {
      yield call(Api.storeItem({token}))
      yield take('LOGOUT')
      yield call(Api.clearItem('token'))
    }
  }
}

若在 call 在去請求 authorize 時,結果未返回,可是此時用戶又觸發了 LOGOUT 的 action,此時的 LOGOUT 將會被忽略而不被處理,由於 loginFlow 在 authorize 中被堵塞了,沒有執行到 take('LOGOUT')那裏

同時執行多個任務

如若遇到某個場景須要同一時間執行多個任務,好比 請求 users 數據 和 products 數據, 應該使用以下的方式:

import { call } from 'redux-saga/effects'
//同步執行
const [users, products] = yield [
  call(fetch, '/users'),
  call(fetch, '/products')
]

//而不是
//順序執行
const users = yield call(fetch, '/users'),
      products = yield call(fetch, '/products')

當 yield 後面是一個數組時,那麼數組裏面的操做將按照 Promise.all 的執行規則來執行,genertor 會阻塞知道全部的 effects 被執行完成

源碼解讀

在每個使用 redux-saga 的項目中,主文件中都會有以下一段將 sagas 中間件加入到 Store 的邏輯:

const sagaMiddleware = createSagaMiddleware({sagaMonitor})
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)

其中 createSagaMiddleware 是 redux-saga 核心源碼文件 src/middleware.js 中導出的方法:

export default function sagaMiddlewareFactory({ context = {}, ...options } = {}) {
 ...
 
 function sagaMiddleware({ getState, dispatch }) {
    const channel = stdChannel()
    channel.put = (options.emitter || identity)(channel.put)

    sagaMiddleware.run = runSaga.bind(null, {
      context,
      channel,
      dispatch,
      getState,
      sagaMonitor,
      logger,
      onError,
      effectMiddlewares,
    })

    return next => action => {
      if (sagaMonitor && sagaMonitor.actionDispatched) {
        sagaMonitor.actionDispatched(action)
      }
      const result = next(action) // hit reducers
      channel.put(action)
      return result
    }
  }
 ...
 
 }

這段邏輯主要是執行了 sagaMiddleware(),該函數裏面將 runSaga 賦值給 sagaMiddleware.run 並執行,最後返回 middleware。 接着看 runSaga() 的邏輯:

export function runSaga(options, saga, ...args) {
...
  const task = proc(
    iterator,
    channel,
    wrapSagaDispatch(dispatch),
    getState,
    context,
    { sagaMonitor, logger, onError, middleware },
    effectId,
    saga.name,
  )

  if (sagaMonitor) {
    sagaMonitor.effectResolved(effectId, task)
  }

  return task
}

這個函數裏定義了返回了一個 task 對象,該 task 是由 proc 產生的,移步 proc.js:

export default function proc(
  iterator,
  stdChannel,
  dispatch = noop,
  getState = noop,
  parentContext = {},
  options = {},
  parentEffectId = 0,
  name = 'anonymous',
  cont,
) {
  ...
  const task = newTask(parentEffectId, name, iterator, cont)
  const mainTask = { name, cancel: cancelMain, isRunning: true }
  const taskQueue = forkQueue(name, mainTask, end)
  
  ...
  
  next()
  
  return task

  function next(arg, isErr){
  ...
      if (!result.done) {
        digestEffect(result.value, parentEffectId, '', next)
      } 
  ...
  }
}

其中 digestEffect 就執行了 effectTriggerd()runEffect(),也就是執行 effect,其中 runEffect() 中定義了不一樣 effect 執行相對應的函數,每個 effect 函數都在 proc.js 實現了。

除了一些核心方法以外,redux-saga 還提供了一系列的 helper 文件,這些文件的做用是返回一個類 iterator 的對象,便於後續的遍歷和執行, 在此不具體分析。

參考文檔

相關文章
相關標籤/搜索