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被觸發時,我們停止另一個本來會一直運行的任務。