redux-saga 初識

easyhappy發表於2018-03-08

原文連結

,如果感興趣或者對美股感興趣可以加我微信: xiaobei060537, 一起交流 ?。

redux-saga 是一個管理 Redux 應用非同步操作的中介軟體,功能類似redux-thunk + async/await, 它通過建立 Sagas 將所有的非同步操作邏輯存放在一個地方進行集中處理。

redux-saga 的 effects

redux-saga中的 Effects 是一個純文字 JavaScript 物件,包含一些將被 saga middleware 執行的指令。這些指令所執行的操作包括如下三種:

  • 發起一個非同步呼叫(如發一起一個 Ajax 請求)
  • 發起其他的 action 從而更新 Store
  • 呼叫其他的 Sagas

Effects 中包含的指令有很多,具體可以非同步API 參考進行查閱

redux-saga 的特點

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

從 redux-thunk 到 redux-saga

假如現在有一個場景:使用者在登入的時候需要驗證使用者的 username 和 password 是否符合要求。

使用 redux-thunk 實現

獲取使用者資料的邏輯(user.js):

// 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):

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 中:

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 實現如下:

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 的物件,便於後續的遍歷和執行, 在此不具體分析。

參考文件