Skip to content

Instantly share code, notes, and snippets.

@otakustay
Created April 20, 2017 09:20
Show Gist options
  • Save otakustay/6f3c0e4acca8170a8261b7f66281276d to your computer and use it in GitHub Desktop.
Save otakustay/6f3c0e4acca8170a8261b7f66281276d to your computer and use it in GitHub Desktop.
redux-managed-thunk

redux-managed-thunk

redux-managed-thunk是一个redux中间件,基于thunk提供了强大的异步管理功能。

一些想法

该中间件在设计的过程中进行过多次的变更,也在redux-thunkredux-promiseredux-generator之间进行过比较和取舍,最终以现在的形式出现,这其中有着很多的考虑。

为什么需要thunk

redux-promise中,提供给dispatch函数的参数是一个Promise对象,这可以用来处理异步的操作,但是这也同时意味着当dispatch运行时,一个异步的过程已经开始执行了,中间件将对异步的执行时机、顺序等失去控制,这严重影响了应用整体对异步的管理能力。

为什么需要Promise

在一个应用中,异步的逻辑是不可避免的,而对异步的处理方式各个中间件也有不同的思路。最为流行的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.donedispatch.fail函数来标记流程的结束,但这种形式在用户不小心忘记调用这些函数时,依旧无法明确标识出流程的结束,进一步使得对异步流程的管理出现问题(如将所有异步串行化时,因为一个流程无法结束导致所有后续流程不再运行)。

而Promise则存在着一系列的优势,使其成为异步流控制的良好选择:

  • 标准支持,符合所有人的预期。
  • ES2018后由语法上的asyncawait支持,配合Promise.all等方法易于进行异步流控制。

为什么需要迭代器

但是从另一个角度来看,Promise因为其只能有一个确定性的结果而存在着缺陷,而基于Redux构建的应用在一个业务流程中,往往需要分阶段地派发多个Action来对应用状态进行更新,redux-promise的相关Issue也就这问题进行了讨论,但并没有一个很好的解决方案。

对于“一个流程多次派发”这一抽象的概念,迭代器正是一个标准支持的模型,每一次迭代(next())返回一个需要派发的Action可以很容易地组织代码。同时官方的异步迭代器标准则可以将异步和迭代器进行整合,在一个异步的流程中多阶段地返回不同的Action。

redux-generator提供了使用迭代器组织逻辑的中间件,但由于其只接受迭代器,导致简单逻辑编写存在一定的负担。另一方面redux-generator并没有很好地遵循官方异步迭代器的标准(仅支持next().value为Promise,而非next()返回Promise),这致使其与语言标准的契合性存在一定问题。

Thunk类型

在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_TODOSAVE_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。

管理thunk

redux-managed-thunk允许从2个层面对thunk的执行进行管理。

全局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函数将throttlebatch整合在一起,形成的逻辑为“同一时间最多同时运行30个thunk,并且每4个thunk的结果整合在一起进行一次同步的派发”。

你也可以编写自己的take函数,每一个take函数符合以下签名:

// 接收一个take函数,返回一个新的take函数
(next: Function) => Function

通过take函数,我们可以在应用的层面对所有派发的thunk进行管理,如electron应用通过ipc进行通讯时,其速度非常快,则可以简单使用series()进行串行化。

单个thunk派发

对于不同的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函数即可。

乐观UI支持

redux-managed-thunk同时支持乐观UI,只需将一个[Function, Function]形式的数组传递给dispatch函数即可,这个数组分为2项:

  1. 第一项为redux-managed-thunk定义的标准thunk,可以是同步函数、异步函数、同步迭代器、异步迭代器之一。
  2. 第二项同样为一个thunk,但只能是同步函数或同步迭代器,不得包含任何异步逻辑。

当接收到数组时,redux-managed-thunk会优先执行第2个thunk并派发产生的乐观Action更新应用状态,随后当第1个thunk产生第一个Action(如第一次yield)后,之前产生的乐观Action将被回滚,随后再通过新的Action将应用状态更新至最新。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment