Redux Saga介紹
Redux Saga是一個用於處理Redux中的副作用的庫。當你觸發一個action時,應用的狀態會發生變化,你可能需要做一些從這個狀態變化衍生出來的操作。
何時使用Redux Saga
在使用Redux的應用中,當你觸發一個action時,應用的狀態會發生變化。
在這種情況下,你可能需要做一些從這個狀態變化衍生出來的操作。
例如你可能想要:
- 向服務器發送HTTP請求
- 發送WebSocket事件
- 從GraphQL服務器獲取一些數據
- 將數據保存到緩存或瀏覽器本地存儲
這些操作都不是和應用狀態直接相關的,而且它們是異步的,你需要將它們放在與actions或reducers不同的地方(雖然理論上你可以這樣做,但這樣不利於保持代碼庫的整潔)。
這就是Redux Saga這個Redux中間件派上用場的地方。
使用Redux Saga的基本示例
為了展示一些實際的代碼,在講解太多理論之前,我簡要介紹一下在構建一個示例應用時我所面臨的問題,以及我是如何解決這個問題的。
在一個聊天室中,當一個用戶發送消息時,我會立即將該消息顯示在屏幕上,以提供即時的反饋。這是通過一個Redux Action來實現的:
1 | const addMessage = (message, author) => ({ |
並且狀態是通過reducer改變的:
1 | const messages = (state = [], action) => { |
首先,你需要導入Redux Saga,並將一個saga作為中間件應用到Redux Store上來初始化Redux Saga:
1 | // ... |
然後,你需要創建一個中間件並將其應用到我們新創建的Redux Store上:
1 | const sagaMiddleware = createSagaMiddleware() |
最後一步是運行saga。我們導入它並將其傳遞給中間件的run方法:
1 | import handleNewMessage from './sagas' |
我們只需要編寫saga,放入./sagas/index.js
中:
1 | import { takeEvery } from 'redux-saga/effects' |
這段代碼的含義是:每次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被執行。例如:
1 | import { takeEvery } from 'redux-saga/effects' |
當中間件執行handleNewMessage
saga時,它會在yield takeEvery
指令處停止並等待(當然是異步的)ADD_MESSAGE
action被派發。然後它運行它的回調函數,saga可以恢復執行。
基本助手函數
助手函數是對低層次saga API的抽象。
讓我們先介紹一下可以用於運行effect的最基本的幫助函數:
takeEvery()
takeLatest()
take()
put()
call()
takeEvery()
takeEvery()
是其中之一的幫助函數,它在一些示例中使用。
在代碼中:
1 | import { takeEvery } from 'redux-saga/effects' |
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的方式。
1 | yield put({ type: 'INCREMENT' }) |
put()
返回一個普通對象,你可以在測試中輕鬆檢查它(有關測試的更多信息稍後介紹)。
call()
當你想在saga中調用某個函數時,可以使用yielded的普通函數調用,它返回一個promise:
1 | delay(1000) |
但是這對於測試來說不太友好。相反,call()
允許你包裹該函數調用,並返回一個可以輕鬆檢查的對象:
1 | call(delay, 1000) |
返回
1 | { CALL: {fn: delay, args: [1000]}} |
並行運行Effect
使用all()
和race()
可以並行運行effect,它們在做什麼方面非常不同。
all()
如果你編寫了這樣的代碼:
1 | import { call } from 'redux-saga/effects' |
第二個fetch()
調用不會被執行,直到第一個fetch()
成功為止。
要並行執行它們,可以將它們包裝到all()
中:
1 | import { all, call } from 'redux-saga/effects' |
all()
直到call()
返回的所有結果都解析後才會解析。
race()
race()
與all()
的不同之處在於它不等待所有helper調用返回。它只等待其中之一返回,然後完成。
這是一場比賽,看哪個先完成,然後我們就忘記其他參與者。
通常用於取消一個永遠運行直到某些事件發生的後台任務:
1 | import { race, call, take } from 'redux-saga/effects' |
當CANCEL_TASK
action被觸發時,我們停止另一個本來會一直運行的任務。