第一次看到这个单词的时候一脸懵逼,由于字典上查到的意思彻底驴头不对马嘴。。。数据库
实际上,这个术语出自康奈尔大学的一篇论文: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)
复制代码
今天就介绍到这里,以一张思惟导图结束本篇文章: