背景
Redux是React的一个状态管理工具,通过单一状态树以及将状态修改操作聚拢到reducer的方式实现状态管理的 可预测 。但整个过程都是 同步的 ,Redux本身并没有内置提供异步操作数据的机制,所以需要中间件来进行这部分的任务,其中最为著名的两个插件便是 redux-thunk , redux-saga 。
redux-thunk
redux-thunk的基本思路是将 action 定义为一个接受 dispatch 和 getState 为参数的函数,以此实现在 action 当中对 dispatch 执行时机的控制。简单的应用如下:
// actionCreator.jsconst incrementAfter = (ms) => (dispatch, getState) => {setTimeout(() => {dispatch({type: 'INCREMENT'})}, ms)}}const increment = () => {return {type: 'INCREMENT'}}const store = createStore(reducers, applyMiddleware(thunk))// 1000ms后加1store.dispatch(incrementAfter(1000))
而redux-thunk中间件的 实现原理 也非常简单,通过判断 action 的类型来确定 dispatch 跟 action 的调用关系,如果 action 为函数,则 dispatch 作为参数传入,反之, action 作为 dispatch 的参数传入。
function createThunkMiddleware(extraArgument) {return ({ dispatch, getState }) => (next) => (action) => {if (typeof action === 'function') {return action(dispatch, getState, extraArgument);}return next(action);};}
redux-thunk的优缺点都非常明显,优点是使用起来非常简单,只需要定义相关的异步函数即可,缺点也在于简单,面对复杂一点的异步流程(譬如打断的情况)缺乏内置的管理方法,导致action难以管理的情况。
redux-saga
saga可以理解为一个专门处理副作用的线程(这个线程并非真实存在),redux-saga使用了ES6 generator函数作为处理副作用的方式,,所以这个专门处理副作用的函数称为 saga函数 。
redux-saga的 基本原理 是通过内部提供的一系列 副作用函数 (call, put等等),生成对应的 副作用描述符(Effect descriptor) ,将此副作用描述符交给 中间件 执行。也就是说,redux-saga的 副作用的创建与执行是分离 的,这么做的一个显著优势就是使得 测试变得非常方便 ,测试副作用的正确性只需要判断对应的描述符即可。
除此之外,redux-saga本身内置了丰富多样的处理副作用的函数,能够 轻松构建复杂的异步控制流 ,这对比redux-thunk来说是一个显著的优势。
基本API
take
等待一个action或者channel,这意味着generator函数会被暂停,直至相应action被dispatch才会继续。这个函数的语义是 等待 ,不同于takeEvery的 被动地 每次触发( pushed模式 ),使用take的saga函数是 主动 获取对应的action或者channel,也能决定继续监听action还是取消监听( pull模式 )。
得益于这种 pull模式 ,我们的saga函数能够使用take实现复杂的控制流,譬如以下的登入登出的控制流,下面的控制流满足了几个需求:
- 获取登陆请求后进行认证
- 进行认证的过程 不能阻塞 登出操作
- 认证成功后保存 认证信息
- 登出时如果存在正在进行登陆认证,则需要 取消
- 登出或者认证失败则需要清空认证信息,然后 等待 下一轮登陆请求
在这个例子当中可以清晰地看到 redux-saga 在构建复杂的异步控制流方面的强大能力,这在 redux-thunk 中是无法想象的。
function* authorize(user, password) {try {const token = yield call(Api.authorize, user, password)yield put({type: 'LOGIN_SUCCESS', token})yield call(Api.storeItem, {token})return token} catch(error) {yield put({type: 'LOGIN_ERROR', error})} finally {if (yield cancelled()) {// ... put special cancellation handling code here}}}function* loginFlow() {while (true) {const {user, password} = yield take('LOGIN_REQUEST')// fork return a Task objectconst task = yield fork(authorize, user, password)const action = yield take(['LOGOUT', 'LOGIN_ERROR'])if (action.type === 'LOGOUT')yield cancel(task)yield call(Api.clearItem, 'token')}}
takeEvery
处理每个符合条件的action或channel,每次命中都会执行对应的saga函数
const takeEvery = (pattern, saga, ...args) => fork(function*() {while (true) {const action = yield take(pattern)yield fork(saga, ...args.concat(action))}})
takeLatest
处理最近一个符合条件的action或channel,如果前一个saga没完成,则会将其终止
适用于处理连续重复发送的ajax请求
const takeLatest = (pattern, saga, ...args) => fork(function*() {let lastTaskwhile (true) {const action = yield take(pattern)if (lastTask) {yield cancel(lastTask) // cancel is no-op if the task has already terminated}lastTask = yield fork(saga, ...args.concat(action))}})
函数调用
call
阻塞调用 函数,直至函数返回saga函数才会继续向下执行。一般用于等待请求结果后改变请求状态的情景。
fork
非阻塞调用 函数,函数的调用并不会阻塞saga函数的执行。可使用 cancel 函数进行取消。
组合
all
类似Promise.all,多个saga函数并行调用,一个错误其他则停止,所有saga完成后再进行下一步。典型应用是并行请求。
race
类似Promise.race,一个saga结束(无论成功失败)后立即下一步,一个典型应用就是请求超时。
actionChannel
主要特性是能够 缓存 没来得及处理的action(譬如call)。
对于处理 无阻塞并行请求 的情况,一般的操作是通过 take+fork 的形式来实现:
function *watchRequest() {while(true) {const {payload} = yield take('REQUEST')yield fork(handleRequest, payload)}}
这种方式的局限在于无法实现将请求 队列化 ,意思是将接收到的请求逐个处理(前一请求完毕后再进行下一请求)。
而actionChannel能将没有来得及处理的action缓存下来,以下代码为例:
function *watchRequest() {const channel = yield(actionChannel('REQUEST'))while(true) {const {payload} = yield take(channel)yield call(handleRequest, payload)}}
在上面的代码当中,take接受的参数是一个actionChannel,actionChannel将所有的action以队列的形式进行缓存,当执行take时,则会返回队头action,得到参数后由call调用,当call调用完成后,则会继续从channel当中获取action进行处理。
eventChannel
eventChannel与actionChannel类似,都能作为 数据源 。eventChannel的 订阅函数 有一个emmit参数,功能是发送数据,其返回值则是一个取消订阅的函数。
function countdown(secs) {return eventChannel(emitter => {const iv = setInterval(() => {secs -= 1if (secs > 0) {emitter(secs)} else {// this causes the channel to closeemitter(END)}}, 1000);// The subscriber must return an unsubscribe functionreturn () => {clearInterval(iv)}})}export function* saga() {const chan = yield call(countdown, value)try {while (true) {// take(END) will cause the saga to terminate by jumping to the finally blocklet seconds = yield take(chan)console.log(`countdown: ${seconds}`)}} finally {console.log('countdown terminated')}}
在上述例子当中,countDown返回一个eventChannel,此eventChannel能作为saga函数的数据源,每一秒钟输出一个数据,当数据为 END 时,当前saga会被终止,进入到finally块当中。
总结
- redux-thunk优点是使用简单,容易上手,缺点是面对复杂的异步流程有点力不从心,因此适用于异步流程简单的业务场景
- redux-saga优点是拥有各种内置的异步流程控制工具,能够轻松构建出复杂的异步流程,缺点是学习成本高,上手需要一定的时间,适用于各种业务场景,不考虑上手成本应该优先使用redux-saga。
参考资料
https://www.vhudyma-blog.eu/2020-07-25-redux-thunk-vs-redux-saga-the-differences/
https://redux-saga.js.org/docs/introduction/BeginnerTutorial.html
https://stackoverflow.com/questions/34930735/pros-cons-of-using-redux-saga-with-es6-generators-vs-redux-thunk-with-es2017-asy
