redux-managed-thunk是一个redux中间件,基于thunk提供了强大的异步管理功能。
该中间件在设计的过程中进行过多次的变更,也在redux-thunk、redux-promise、redux-generator之间进行过比较和取舍,最终以现在的形式出现,这其中有着很多的考虑。
在redux-promise中,提供给dispatch
函数的参数是一个Promise对象,这可以用来处理异步的操作,但是这也同时意味着当dispatch
运行时,一个异步的过程已经开始执行了,中间件将对异步的执行时机、顺序等失去控制,这严重影响了应用整体对异步的管理能力。
在一个应用中,异步的逻辑是不可避免的,而对异步的处理方式各个中间件也有不同的思路。最为流行的redux-thunk选择提供一个dispatch
参数以供随时调用,但其带来的代价是由于dispatch
在任意时刻均可调用,因此无法知晓一个逻辑何时结束,其结果是成功还是失败。
因此我们需要有一个模式去标记流程的结束和结果成功与否,一种基于thunk的简单扩展方案是提供额外的回调函数:
let saveTodo = todo => async dispatch => {
dispatch({type: 'SAVE_START'});
let savedTodo = await post('/todos', todo);
// 使用dispatch.done标记结束
dispatch.done({type: 'ADD_TODO', palyoad: savedTodo});
};
通过提供dispatch.done
和dispatch.fail
函数来标记流程的结束,但这种形式在用户不小心忘记调用这些函数时,依旧无法明确标识出流程的结束,进一步使得对异步流程的管理出现问题(如将所有异步串行化时,因为一个流程无法结束导致所有后续流程不再运行)。
而Promise则存在着一系列的优势,使其成为异步流控制的良好选择:
- 标准支持,符合所有人的预期。
- ES2018后由语法上的
async
和await
支持,配合Promise.all
等方法易于进行异步流控制。
但是从另一个角度来看,Promise因为其只能有一个确定性的结果而存在着缺陷,而基于Redux构建的应用在一个业务流程中,往往需要分阶段地派发多个Action来对应用状态进行更新,redux-promise的相关Issue也就这问题进行了讨论,但并没有一个很好的解决方案。
对于“一个流程多次派发”这一抽象的概念,迭代器正是一个标准支持的模型,每一次迭代(next()
)返回一个需要派发的Action可以很容易地组织代码。同时官方的异步迭代器标准则可以将异步和迭代器进行整合,在一个异步的流程中多阶段地返回不同的Action。
redux-generator提供了使用迭代器组织逻辑的中间件,但由于其只接受迭代器,导致简单逻辑编写存在一定的负担。另一方面redux-generator并没有很好地遵循官方异步迭代器的标准(仅支持next().value
为Promise,而非next()
返回Promise),这致使其与语言标准的契合性存在一定问题。
在redux-managed-thunk中,你可以通过dispatch
派发函数,这一点上与redux-thunk一致。但是redux-managed-thunk接受更多形式的函数类型,你可以提供以下形式的函数:
- 一个普通的同步函数,返回一个Action,这个Action会在返回后被同步派发。
- 一个异步函数,返回一个Promise,当这个Promise进入resolve状态时,提供的Action会被派发。
- 一个返回迭代器(Iterator)的函数,该迭代器的每一次迭代(如使用
yield
)产生一个Action,并在流程结束后返回(return
)一个Action,这些Action将依次被派发。
当thunk返回迭代器时,每次迭代同样可以返回一个Promise,进行异步的Action派发:
import {post} from 'http-api';
let saveToRemove = async todo => {
let savedTodo = await post('/todos', todo);
return {type: 'ADD_TODO', palyoad: savedTodo};
};
let saveTodo = todo => function* () {
yield({type: 'SAVE_START'});
yield saveToRemove(todo);
yield {type: 'SAVE_DONE'};
}
但是大部分时候这样组织代码会比较麻烦,如为了创建Promise产生额外的代码或函数,因此开发者可以选择异步迭代器进行逻辑编写:
let saveTodo = todo => async function* () {
yield({type: 'SAVE_START'});
let savedTodo = await post('/todos', todo);
yield {type: 'ADD_TODO', palyoad: savedTodo};
yield {type: 'SAVE_DONE'};
}
但需要注意的是,异步迭代器中的每两个yield
之间都是异步的,上面的代码中ADD_TODO
和SAVE_DONE
也将异步被派发,这可能导致一些状态竞争等异步问题,因此并不推荐在需要连续派发多个Action时使用异步迭代器。另一种解决方案是使用redux-batch-middleware等中间件将Action合并起来:
let saveTodo = todo => async function* () {
yield({type: 'SAVE_START'});
let savedTodo = await post('/todos', todo);
// redux-batch-middleware接收数组并同步派发所有Action
yield [{type: 'ADD_TODO', palyoad: savedTodo}, {type: 'SAVE_DONE'}];
}
另一点与redux-thunk不同的是,redux-managed-thunk不会给thunk传递dispatch
参数,仅有的参数为(getState, [extraArgument])
,你需要使用函数的返回值、Promise的resolve参数或者迭代器的迭代值提供Action。
redux-managed-thunk允许从2个层面对thunk的执行进行管理。
redux-managed-thunk允许自定义take
函数对所有派发的thunk的执行方式进行管理,在调用managedThunk
创建中间件时提供参数即可,如:
import {managedThunk, series} from 'redux-managed-thunk';
import {applyMiddleware} from 'redux';
applyMiddleWare(managedThunk(null, {take: series()}));
以上代码使用series
处理所有派发的thunk,其逻辑是将逻辑的执行串行化,前一个thunk结束前,后续的thunk必须排队等待。
除此之外,可以使用更多的函数组织对thunk的派发逻辑:
import {managedThunk, throttle, batch} from 'redux-managed-thunk';
import {applyMiddleware, compose} from 'redux';
applyMiddleWare(managedThunk(null, {take: compose(batch(4), throttle(30))}));
以上代码通过compose
函数将throttle
和batch
整合在一起,形成的逻辑为“同一时间最多同时运行30个thunk,并且每4个thunk的结果整合在一起进行一次同步的派发”。
你也可以编写自己的take
函数,每一个take
函数符合以下签名:
// 接收一个take函数,返回一个新的take函数
(next: Function) => Function
通过take
函数,我们可以在应用的层面对所有派发的thunk进行管理,如electron应用通过ipc进行通讯时,其速度非常快,则可以简单使用series()
进行串行化。
对于不同的thunk,我们有时候也需要对其进行管理,常见的有:
- 对于类似GET只读资源的请求,如果同时调用N次,那么基于幂等性(虽然往往不现实),可以复用第一次请求的结果。
- 对于类似PUT的更新请示,如果同时调用N次,那么应该最后一次生效,前面的调用可以被取消。
我们可以使用对thunk函数进行转换的高阶函数来实现这些:
import {reusePrevious, cancelPrevious} from 'redux-managed-thunk';
import {get, put} from 'http-api';
let fetchTodo = id => async () => get(`/todos/${id}`);
// 复用之前已经开始的thunk
fetchTodo = reusePrevious(fetchTodo);
let updateTodo = todo => async () => put(`/todos/${todo.id}`, todo);
// 取消之前开始的thunk
updateTodo = cancelPrevious(updateTodo);
使用不同的高阶函数可以产生不同的效果,如使用了cancelPrevious
之后,当新的thunk开始执行时,之前未执行完的thunk所产生的Action都将被忽略(已产生的Action则已经生效)。
同样,可以编写自己的高阶函数,只需接收一个thunk并返回一个新的thunk函数即可。
redux-managed-thunk同时支持乐观UI,只需将一个[Function, Function]
形式的数组传递给dispatch
函数即可,这个数组分为2项:
- 第一项为redux-managed-thunk定义的标准thunk,可以是同步函数、异步函数、同步迭代器、异步迭代器之一。
- 第二项同样为一个thunk,但只能是同步函数或同步迭代器,不得包含任何异步逻辑。
当接收到数组时,redux-managed-thunk会优先执行第2个thunk并派发产生的乐观Action更新应用状态,随后当第1个thunk产生第一个Action(如第一次yield
)后,之前产生的乐观Action将被回滚,随后再通过新的Action将应用状态更新至最新。