Redux Saga是一個用於處理Redux中的副作用的庫。當你觸發一個action時,應用的狀態會發生變化,你可能需要做一些從這個狀態變化衍生出來的操作。

何時使用Redux Saga

在使用Redux的應用中,當你觸發一個action時,應用的狀態會發生變化。

在這種情況下,你可能需要做一些從這個狀態變化衍生出來的操作。

例如你可能想要:

  • 向服務器發送HTTP請求
  • 發送WebSocket事件
  • GraphQL服務器獲取一些數據
  • 將數據保存到緩存或瀏覽器本地存儲

這些操作都不是和應用狀態直接相關的,而且它們是異步的,你需要將它們放在與actions或reducers不同的地方(雖然理論上你可以這樣做,但這樣不利於保持代碼庫的整潔)。

這就是Redux Saga這個Redux中間件派上用場的地方。

使用Redux Saga的基本示例

為了展示一些實際的代碼,在講解太多理論之前,我簡要介紹一下在構建一個示例應用時我所面臨的問題,以及我是如何解決這個問題的。

在一個聊天室中,當一個用戶發送消息時,我會立即將該消息顯示在屏幕上,以提供即時的反饋。這是通過一個Redux Action來實現的:

const addMessage = (message, author) => ({
 type: 'ADD\_MESSAGE',
 message,
 author
})

並且狀態是通過reducer改變的:

const messages = (state = [], action) => {
 switch (action.type) {
 case 'ADD\_MESSAGE':
 return state.concat([{
 message: action.message,
 author: action.author
 }])
 default:
 return state
 }
}

首先,你需要導入Redux Saga,並將一個saga作為中間件應用到Redux Store上來初始化Redux Saga:

// ...
import createSagaMiddleware from 'redux-saga'
// ...

然後,你需要創建一個中間件並將其應用到我們新創建的Redux Store上:

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
 reducers,
 applyMiddleware(sagaMiddleware)
)

最後一步是運行saga。我們導入它並將其傳遞給中間件的run方法:

import handleNewMessage from './sagas'
// ...
sagaMiddleware.run(handleNewMessage)

我們只需要編寫saga,放入./sagas/index.js中:

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

const handleNewMessage = function* handleNewMessage(params) {
 const socket = new WebSocket('ws://localhost:8989')
 yield takeEvery('ADD\_MESSAGE', (action) => {
 socket.send(JSON.stringify(action))
 })
}

export default handleNewMessage

這段代碼的含義是:每次ADD_MESSAGE action觸發時,我們都會向WebSocket服務器發送一個消息。在這種情況下,WebSocket服務器的響應在localhost:8989上進行。

請注意,我們使用function*來定義這個saga,這不是一個普通的函數,而是一個生成器

幕後工作原理

Redux Saga作為一個Redux中間件,可以攔截Redux Actions,並注入自己的功能。

有一些概念需要理解,下面是你需要記住的主要關鍵詞:saga, generator, middleware, promise, pause, resume, effect, dispatch, action, fulfilled, resolved, yield, yielded。

一個saga是對你的代碼產生的一個effect做出反應的“故事”。這可能包含我們之前談到的HTTP請求或一些保存到緩存的操作。

我們通過一個包含一個或多個saga的列表創建一個中間件,並將該中間件連接到Redux store上。

一個saga是一個generator function。當一個promise被執行並被yielded時,中間件會暫停saga直到promise被解析。

一旦promise被解析,中間件會繼續執行saga,直到找到下一個yield語句,然後再次暫停,直到promise被解析。

在saga代碼中,你會使用redux-saga包提供的一些特殊輔助函數生成effects。開始時,我們可以列舉出以下幾個:

  • takeEvery()
  • takeLatest()
  • take()
  • call()
  • put()

當一個effect被執行時,saga會暫停,直到此effect被執行。例如:

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

const handleNewMessage = function* handleNewMessage(params) {
 const socket = new WebSocket('ws://localhost:8989')
 yield takeEvery('ADD\_MESSAGE', (action) => {
 socket.send(JSON.stringify(action))
 })
}

export default handleNewMessage

當中間件執行handleNewMessage saga時,它會在yield takeEvery指令處停止並等待(當然是異步的)ADD_MESSAGE action被派發。然後它運行它的回調函數,saga可以恢復執行。

基本助手函數

助手函數是對低層次saga API的抽象。

讓我們先介紹一下可以用於運行effect的最基本的幫助函數:

  • takeEvery()
  • takeLatest()
  • take()
  • put()
  • call()

takeEvery()

takeEvery()是其中之一的幫助函數,它在一些示例中使用。

在代碼中:

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

function* watchMessages() {
 yield takeEvery('ADD\_MESSAGE', postMessageToServer)
}

watchMessages生成器將在ADD_MESSAGE action被觸發時暫停,每次它被觸發,都會調用postMessageToServer函數,並且是無限次並發運行的(在新的運行之前,不需要等待postMessageToServer執行完畢)

takeLatest()

另一個常用的幫助函數是takeLatest(),它與takeEvery()非常相似,但只允許一個處理函數同時運行,從而避免並發。如果在處理函數運行時另一個action被觸發,它會取消運行中的處理函數,並使用最新的數據重新運行。

takeEvery()一樣,生成器不會停止,並在指定的action發生時繼續運行effect。

take()

take()不同的地方在於它只等待一次。當等待的action發生時,promise解析並且迭代器繼續,進行下一組指令。

put()

將一個action分派給Redux store。你可以直接使用put()而不是將Redux store或dispatch action傳遞給saga的方式。

yield put({ type: 'INCREMENT' })
yield put({ type: "USER\_FETCH\_SUCCEEDED", data: data })

put()返回一個普通對象,你可以在測試中輕鬆檢查它(有關測試的更多信息稍後介紹)。

call()

當你想在saga中調用某個函數時,可以使用yielded的普通函數調用,它返回一個promise:

delay(1000)

但是這對於測試來說不太友好。相反,call()允許你包裹該函數調用,並返回一個可以輕鬆檢查的對象:

call(delay, 1000)

返回

{ CALL: {fn: delay, args: [1000]}}

並行運行Effect

使用all()race()可以並行運行effect,它們在做什麼方面非常不同。

all()

如果你編寫了這樣的代碼:

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

const todos = yield call(fetch, '/api/todos')
const user = yield call(fetch, '/api/user')

第二個fetch()調用不會被執行,直到第一個fetch()成功為止。

要並行執行它們,可以將它們包裝到all()中:

import { all, call } from 'redux-saga/effects'

const [todos, user] = yield all([
 call(fetch, '/api/todos'),
 call(fetch, '/api/user')
])

all()直到call()返回的所有結果都解析後才會解析。

race()

race()all()的不同之處在於它不等待所有helper調用返回。它只等待其中之一返回,然後完成。

這是一場比賽,看哪個先完成,然後我們就忘記其他參與者。

通常用於取消一個永遠運行直到某些事件發生的後台任務:

import { race, call, take } from 'redux-saga/effects'

function* someBackgroundTask() {
 while(1) {
 //...
 }
}

yield race([
 bgTask: call(someBackgroundTask),
 cancel: take('CANCEL\_TASK')
])

CANCEL_TASK action被觸發時,我們停止另一個本來會一直運行的任務。