第一次看到這個單詞的時候一臉懵逼,由於字典上查到的意思徹底驢頭不對馬嘴。。。數據庫
實際上,這個術語出自康奈爾大學的一篇論文:www.cs.cornell.edu/andru/cs711…express
最初這篇論文是爲了解決分佈式系統中的LLT(Long Lived Transaction),也就是長時運行事務的數據一致性問題的。這麼說有點抽象,咱們來舉個具體的例子:編程
假如你在一個在線訂票系統上訂了一張機票,下單成功後,通常會給你留30分鐘付款時間,若是你在30分鐘內完成付款就能夠成功出票,不然會被取消預約。也就是說,從下單到出票,最長可能須要30分鐘,這就是傳說中的LLT。用過數據庫的同窗確定都知道,所謂「事務(Transaction)」,指的是一個原子操做,要麼所有執行,要麼所有回滾。那麼問題來了,爲了保證數據的一致性,咱們是否是應該等待剛纔那個LLT執行完成呢?這顯然不現實,由於這意味着在這30分鐘內,其餘人都沒辦法訂票了。。。因而,在1987年,康奈爾大學的兩位大佬發表了一篇論文,提出了一個新的概念,叫作saga:redux
Let us use the term saga to refer to a LLT that can be broken up into a collection of sub-transactions that can be interleaved in any way with other transactionsbash
具體是什麼意思呢?仍是以上面的訂票系統爲例,兩位大佬說了,咱們能夠把這個LLT拆成兩個子事務嘛,T1表示「預約」事務,T2表示「出票」事務。先執行T1,而後就能夠把數據庫釋放出來了,其餘人也能夠正常訂票了。若是用戶在30分鐘內完成了付款,那麼再執行T2完成出票,這樣整個事務就執行完畢了。假如超過了30分鐘用戶尚未付款怎麼辦?這時候須要執行一個「補償」事務C1,用來回滾T1對數據庫形成的修改。這幾個子事務組合在一塊兒,就叫一個saga:網絡
固然,上面的例子只是最簡單的狀況,實際應用中的LLT可能很是複雜,包含很是多的子事務:app
另外還有更復雜的並行saga,這裏就不介紹了。看到這裏,你可能會以爲,這好像也沒啥嘛,原本就應該這麼作啊。是的,若是你早出生30年,沒準發論文的就就是你了^_^異步
還須要再介紹一個概念:反作用(Side Effect)。分佈式
若是有一天我跟你說你提交的代碼有side effect,其實我是在委婉地說,你的代碼搞出bug來了。。。固然,這跟咱們這裏討論的side effect不是一回事兒。咱們這裏討論的side effect出自於「函數式編程」,這種編程範式鼓勵咱們多使用「純函數」。所謂純函數,指的是一個函數知足如下兩個特色:ide
爲何要多用純函數呢?由於它們具備很強的「可預測性」。既然有純函數,那確定有不純的函數嘍,或者換個說法,叫作有「反作用」的函數。咱們能夠看一下維基百科上的定義:
In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the main effect) to the invoker of the operation.
顯然,大多數的異步任務都須要和外部世界進行交互,不論是發起網絡請求、訪問本地文件或是數據庫等等,所以,它們都會產生「反作用」。
redux-saga是一個Redux中間件,用來幫你管理程序的反作用。或者更直接一點,主要是用來處理異步action。
上一篇咱們介紹過Redux的中間件,說白了就是在action被傳遞到reducer以前新進行了一次攔截,而後啓動異步任務,等異步任務執行完成後再發送一個新的action,調用reducer修改狀態數據。redux-saga的功能也是同樣的,參見下圖:
左邊的藍圈圈裏就是一堆saga,它們須要和外部進行異步I/O交互,等交互完成後再修改Store中的狀態數據。redux-saga就是一個幫你管理這堆saga的管家,那麼它跟其餘的中間件實現有什麼不一樣呢?它使用了ES6中Generator函數語法。
Javascript的語法一直在演進,其中最爲重要的因素之一就是爲了簡化異步調用的書寫方式。
從最初的callback「回調地獄」:
step1(value0, function(value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
console.log(value3)
})
})
})
複製代碼
到後來的Promise鏈式調用:
step1(value0)
.then(value1 => step2(value1))
.then(value2 => step3(value2))
.then(value3 => console.log(value3))
.catch(error => console.log(error))
複製代碼
再到ES6中引入的Generator函數:
function* mySaga(value0) {
try {
var value1 = yield step1(value0)
var value2 = yield step2(value1)
var value3 = yield step3(value2)
console.log(value3)
} catch(e) {
console.log(e)
}
}
複製代碼
能夠看到,Generator函數的寫法基本上和同步調用徹底同樣了,惟一的區別是function後面有個星號,另外函數調用以前須要加上一個yield關鍵字。
看起來彷佛很完美,可是實際上沒有這麼簡單。下面這張圖描述了Generator函數的實際調用流程:
var it = mySaga(value0)
it.next()
複製代碼
另外,當step1()執行完異步任務後,須要再次調用it.next()才能繼續執行下一個yield後面的異步函數。因此step1()可能會相似下面這個樣子,step2()/step3()也是同樣:
const step1 = (value0) => {
makeAjaxCall(value0)
.then(response => it.next(response))
}
複製代碼
不過,幸運的是,redux-saga已經幫咱們封裝好了這一切,你只要專心實現異步調用邏輯就能夠了。
根據上一節的分析,咱們不只須要實現一個Generator函數,還須要提供一個外部驅動函數。這在redux-saga中被稱爲worker saga和watcher saga:
咱們來看一個具體的例子:
import Api from '...'
function* workerSaga(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED", message: e.message});
}
}
function* watcherSaga() {
yield takeEvery("USER_FETCH_REQUESTED", workerSaga);
}
複製代碼
咱們先看一下watcherSaga:watcherSaga中使用了redux-saga提供的API函數takeEvery(),當有接收到USER_FETCH_REQUESTED action時,會啓動worker saga。另外一個經常使用的輔助函數時takeLatest(),當有相同的action發送過來時,會取消當前正在執行的任務並從新啓動一個新的worker saga。
而後咱們看下workerSaga,能夠看到並非直接調用異步函數或者派發action,而是經過call()以及put()這樣的函數。這就是redux-saga中最爲重要的一個概念:Effect。
實際上,咱們能夠直接經過yield fetchUser()執行咱們的異步任務,並返回一個Promise。可是這樣的話很差作模擬(mock)測試:咱們在測試過程當中,通常不會真的執行異步任務,而是替換成一個假函數。實際上,咱們只須要確保yield了一個正確的函數,而且函數有着正確的參數。
所以,相比於直接調用異步函數,咱們能夠僅僅 yield 一條描述函數調用的指令,由redux-saga中間件負責解釋執行該指令,並在得到結果響應時恢復Generator的執行。這條指令是一個純Javascript對象(相似於action):
{
CALL: {
fn: Api.fetchUser,
args: ['alice']
}
}
複製代碼
這樣,當咱們須要測試Generator函數時,就能夠用一條簡單的assert語句來比較兩個Effect對象(即便不在Redux環境中):
assert.deepEqual(
iterator.next().value,
call(Api.fetchUser, 'alice'),
"Should yield an Effect call(Api.fetchUser, 'alice')"
)
複製代碼
爲了實現這一目標,redux-saga提供了一系列API函數來生成Effect對象,比較經常使用的是下面這幾個:
好比咱們以前用到的takeEvery()函數,其實內部實現就是不停地take -> fork -> take -> fork …循環。當接收到指定action時,會啓動一個worker saga,並驅動其中的yield調用。
借用網上的一張神圖來更直觀地理解上面這些API的做用:
另外,若是你想要同時監聽不一樣的action,可使用all()或者race()把他們組合成一個root saga:
export default function* rootSaga() {
yield all([
takeEvery("FOO_ACTION", fooASaga),
takeEvery("BAR_ACTION", barASaga)
])
}
複製代碼
最後,你須要在createStore()時註冊redux-saga中間件,而後調用run()函數啓動你的root saga就大功告成了:
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import reducer from './reducers'
import rootSaga from './sagas'
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
// then run the saga
sagaMiddleware.run(rootSaga)
複製代碼
今天就介紹到這裏,以一張思惟導圖結束本篇文章: