前端技術 | redux-saga,化異步爲同步

1.什麼是Saga?

第一次看到這個單詞的時候一臉懵逼,由於字典上查到的意思徹底驢頭不對馬嘴。。。數據庫

實際上,這個術語出自康奈爾大學的一篇論文: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年,沒準發論文的就就是你了^_^異步

2.反作用(Side Effect)

還須要再介紹一個概念:反作用(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.

顯然,大多數的異步任務都須要和外部世界進行交互,不論是發起網絡請求、訪問本地文件或是數據庫等等,所以,它們都會產生「反作用」。

3.什麼是redux-saga?

redux-saga是一個Redux中間件,用來幫你管理程序的反作用。或者更直接一點,主要是用來處理異步action。

上一篇咱們介紹過Redux的中間件,說白了就是在action被傳遞到reducer以前新進行了一次攔截,而後啓動異步任務,等異步任務執行完成後再發送一個新的action,調用reducer修改狀態數據。redux-saga的功能也是同樣的,參見下圖:

左邊的藍圈圈裏就是一堆saga,它們須要和外部進行異步I/O交互,等交互完成後再修改Store中的狀態數據。redux-saga就是一個幫你管理這堆saga的管家,那麼它跟其餘的中間件實現有什麼不一樣呢?它使用了ES6中Generator函數語法

4.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函數的實際調用流程:

當你調用mySaga()時,其實並無真正執行函數,而只是返回了一個迭代器(Iterator)。你必需要經過迭代器的next()函數才能執行第1個yield後面的step1()函數:

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已經幫咱們封裝好了這一切,你只要專心實現異步調用邏輯就能夠了。

5.redux-saga用法

根據上一節的分析,咱們不只須要實現一個Generator函數,還須要提供一個外部驅動函數。這在redux-saga中被稱爲worker sagawatcher saga

  • worker saga:具體業務邏輯實現
  • watcher saga:接收特定的action,而後驅動worker 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對象,比較經常使用的是下面這幾個:

  • call:函數調用
  • select:獲取Store中的數據
  • put:向Store發送action
  • take:在Store上等待指定action
  • fork:和call相似,可是是非阻塞的,當即返回

好比咱們以前用到的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)
複製代碼

今天就介紹到這裏,以一張思惟導圖結束本篇文章:

相關文章
相關標籤/搜索