Redux02 异步操作和中间件

学习Redux中间件的概念,以及使用Redux中间件完成异步操作的方法。

同步和异步流程

先来复习一下Redux的基本流程:

1234567
1. 用户发出Action2. Store自动调用Reducer,计算返回一个新的State3. Store就会调用监听函数4. 监听函数listener中重新渲染View

在第1、2步之间有一个问题,之前考虑的情况都是在Action发出之后,Reducer立刻计算出State,这是一个同步的过程。如果在Action发出之后,过一段时间再执行Reducer,这是异步过程:

123456789
1. 用户发出Action1.5 异步操作(等待一段时间)2. Store自动调用Reducer,计算返回一个新的State3. Store就会调用监听函数4. 监听函数listener中重新渲染View

现在我们希望的是在异步操作结束后,自动执行Reducer,这就要用到中间件(middleware)

中间件的概念

什么是中间件?中间件(middleware)是一种很常见、也很强大的模式,被广泛应用在Express、Koa、Redux等类库和框架当中。

简单来说,中间件就是在调用目标函数之前,可以随意插入其他函数预先对数据进行处理、过滤,在这个过程里面你可以打印数据、或者停止往下执行中间件等。数据就像水流一样经过中间件的层层的处理、过滤,最终到达目标函数。

1234
// 中间件可以把 A 发送数据到 B 的形式从// A -----> B// 变成:// A ---> middleware 1 ---> middleware 2 ---> middleware 3 --> ... ---> B

具体到Redux来看,如果要实现中间件,最合适环节就是在发送Action的环节,即使用中间件包裹store.dispatch来添加功能,比如要增加打印功能,将Action和State打印出来,我们就可以编写这样一个中间件:

1234567
const next = store.dispatch;store.dispatch = function (action) {  console.log(‘action: ‘, action);  next(action);  console.log(‘next state: ‘, store.getState())};

中间件对store.dispatch进行了改造,在发出Action和执行Reducer之间添加了其他功能。但是实际上中间件的写法不是这样的。

在Redux中,中间件是纯函数,有明确的使用方法,并且要严格的遵循以下格式:

1234567
var anyMiddleware = function ({ dispatch, getState }) {  return function(next) {    return function (action) {          }  }}

中间件由三个嵌套的函数构成(会依次调用):

(1)第一层向其余两层提供分发函数dispatchgetState函数

(2)第二层提供next函数,它允许你显示的将处理过的输入传递给下一个中间件或Redux(这样Redux才能调用所有reducer)。实际上next作为参数,就是通过componse传入的下一个要执行的函数,通过next(action)就将action传递给了下一中间件

(3)第三层提供从上一个中间件或者从dispatch传递过来的Action,这个Action可以调用下一个中间件(让Action继续流动)或者以想要的方式处理action

所以一个Log的中间件应该这样写:

12345678
function  ({ dispatch, getState }) {  return function(next) {    return function (action) {      console.log(‘logMiddleware action received:‘, action)      return next(action)    }  }}

next(action)就是继续传递Action,如果不进行这一步,所有的Action都会被丢弃。

中间件的用法

常用的中间件都有现成的,不用我们自行编写,只需要直接引用别人写好的模块即可,比如上面的打印日志的中间件,就可以使用现成的redux-logger模块:

12345678
import { applyMiddleware, createStore } from ‘redux‘;import createLogger from ‘redux-logger‘;const logger = createLogger();const store = createStore(  reducer,  applyMiddleware(logger));

使用的时候首先通过redux-logger提供的生成方法createLogger创建一个中间件实例logger,然后将它放在Redux提供的applyMiddleware方法中,放到createStore方法中(由于createStore方法可以接受应用的初始状态作为第二个参数,这个时候applyMiddleware方法就是第三个参数了)

有的中间件有次序要求,必须放在何时的位置才能正确输出,使用之前要查看文档。

applyMiddleware()

applyMiddleware()是Redux的原生方法,会将所有中间件组成一个数组,依次执行,下面是它的源码:

12345678910111213141516
export default function (...middlewares) {  return (createStore) => (reducer, preloadedState, enhancer) => {    var store = createStore(reducer, preloadedState, enhancer);    var dispatch = store.dispatch;    var chain = [];    var middlewareAPI = {      getState: store.getState,      dispatch: (action) => dispatch(action)    };    chain = middlewares.map(middleware => middleware(middlewareAPI));    dispatch = compose(...chain)(store.dispatch);    return {...store, dispatch}  }}

applyMiddleware可以接受多个中间件作为参数,全部放进了数组chain中,每个中间件接受Store的dispatchgetState函数作为命名参数,返回一个函数。该函数会被传入称为next的下一个中间件的dispatch方法,并返回一个接受Action的新函数,这个函数可以直接调用next(action)。这个过程是通过compose方法完成的。

多个中间件形成了一个调用链,调用链中的最后一个中间件会接受真实Store的dispatch作为next参数,并借此结束调用链。

12
({ getState, dispatch }) => next => action

compose()

compose(...functions)的功能是从右到左来组合多个函数,这是函数式编程的方法,其中每个函数的返回值作为参数提供给左边的函数:

123
compose(funcA, funcB, funcC); funcA(funcB(funcC()))

关于compose方法,以前做过一道练习题《前端练习17 函数式编程的compose函数》,手写简易的compose方法。

1234
const store = createStore(  reducer,  applyMiddleware(thunk, promise, logger));

异步操作的基本思路

处理异步操作需要使用中间件。

同步操作只要发出一种Action即可,异步操作的差别是要发出三种Action

123
- 操作发起时的Action- 操作成功时的Action- 操作失败时的Action

以向服务器取出数据为例,三种Action有两种不同的写法:

123456789
{ type: ‘FETCH_POSTS‘ }{ type: ‘FETCH_POSTS‘, status: ‘error‘, error: ‘Oops‘ }{ type: ‘FETCH_POSTS‘, status: ‘success‘, respose: {} }// 写法二, 名称不同{ type: ‘FETCH_POSTS‘ }{ type: ‘FETCH_POSTS_FAILURE‘, error: ‘Oops‘ }{ type: ‘FETCH_POSTS_SUCCESS‘, respose: {} }

除了Action种类不同,异步操作的State也要进行改造,反映不同的操作状态,例如:

123456
const state = {  // ...  isFetching: true,  didInvalidate: true,  lastUpdated: ‘xxxxxxx‘}

State中的属性isFetching表示是否正在抓取数据,didInvalidate表示是否正过期,lastUpdated表示上一次更新事件。

现在整个异步操作的思路就很清晰了:

12
1. 操作开始,发出一个Action,触发State更新为“正在操作”状态,View重新渲染2. 操作结束,再次发出一个Action,触发State更新为“操作结束”状态,View再次重新渲染

redux-thunk中间件

异步操作至少要发出两个Action,用户操作触发第一个Action,这个和同步操作一样,标识着异步操作的开始,现在要做的是在异步操作结束时,自动发送第二个Action

奥妙就在Action Creator中,需要对其进行改造。我们有一个组件,点击按钮后会发出一个Ajax请求,将返回的结果填充在视图中,按钮的点击事件如下:

123456789101112131415大专栏  Redux02 异步操作和中间件r>161718192021
sendQuestion() {  const question = this.state.questionInput;  // Action Creator1  const requestPost = (question) => ({ type: ‘SEND_QUESTION‘, status: ‘sending...‘, question });    // Action Creator2  const receivePost = (answer) => ({ type: ‘RECEIVE_ANSWER‘, status: ‘‘, answer });     // Action Creator3  const actionCreator = () => (dispatch, getState) => {    dispatch(requestPost(question));    // 重置输入框    this.setState({      questionInput: ‘‘    });    return Request.demo2.getAnswer({ question })      .then(res => dispatch(receivePost(res)))  };  store.dispatch(actionCreator())}

其中最关键的就是actionCreator,它的返回值是一个函数,这个函数执行时,会先发出一个ActionrequestPost(由Action Creator生成)并进行其他同步操作,然后进行异步操作Request.demo2.getAnswer({ question }),在异步操作的回调函数中发出第二个ActionactionCreator(由Action Creator2生成)。

上面的代码中,有几点要注意:

(1)完成异步操作的Action CreatoractionCreator返回的是一个函数,普通的Action Creator返回的是Action对象

(2)返回的这个函数参数是dispatchgetState这两个Redux方法,普通的Action Creator参数是Action的内容。

(3)在返回的函数中,先发出的Actiondispatch(requestPost(question))表示操作开始

(4)异步操作结束后,在发出的Actiondispatch(receivePost(res))表示操作结束

第二点中,返回函数的两个Redux方法是执行时由函数的执行者传进去的,函数的执行者是谁呢?就是中间件redux-thunk

为什么要使用redux-thunk?因为Action是由store.dispatch发出的,这个方法接受的参数是一个对象,而我们的Action Creator返回的是一个函数,使用redux-thunk对store.dispatch进行改造,改造后在执行Action Creator返回的函数时就传入了dispatchgetState两个参数

12345678910
import { createStore, applyMiddleware } from "redux";import reducer from "./reducers/index";// 使用thunk中间件,使dispatch可以接受函数作为参数(默认只能接受Action对象作为参数)import thunk from ‘redux-thunk‘;// 创建Storeconst store = createStore(reducer, applyMiddleware(thunk));export default store;

因此,异步操作的第一种解决方案就是,==编写一个返回函数的Action Creator,然后使用redux-thunk中间件改造store.dispatch==

redux-promise中间件

在上面的Action Creator返回了一个函数,也可以返回其他值,另一种异步操作的解决方案,就是让Action Creator返回一个Promise对象

这需要使用redux-promise中间件

12345678910
import { createStore, applyMiddleware } from "redux";import reducer from "./reducers/index";// 使用redux-promise中间件,使dispatch可以接受Promise作为参数import promiseMiddleware from ‘redux-promise‘// 创建Storeconst store = createStore(reducer, applyMiddleware(promiseMiddleware));export default store;

来看一下它的源码:

12345678910111213141516171819202122
import isPromise from ‘is-promise‘;import { isFSA } from ‘flux-standard-action‘;export default function promiseMiddleware({ dispatch }) {  return next => action => {    if (!isFSA(action)) {      return isPromise(action)        ? action.then(dispatch)        : next(action);    }    return isPromise(action.payload)      ? action.payload.then(          result => dispatch({ ...action, payload: result }),          error => {            dispatch({ ...action, payload: error, error: true });            return Promise.reject(error);          }        )      : next(action);  };}

如果Action本身是一个Promise,它resolve后的值是一个Action对象,会被dispatch方法提交,reject后不会有任何动作,如果Action本身不是一个Promise对象,而Action对象的payload属性是一个Promise对象,那么无论其resolve或reject,dispatch都会发出Action

所以有两种写法,一种是让Action本身返回一个Promise对象:

12345678910111213141516171819202122
sendQuestion() {  const question = this.state.questionInput;    // Action Creator1  const requestPost = (question) => ({ type: ‘SEND_QUESTION‘, status: ‘sending...‘, question });    // Action Creator2  const receivePost = async () => ({    type: ‘RECEIVE_ANSWER‘,    status: ‘‘,    answer: await Request.demo2.getAnswer({ question })  });    store.dispatch(requestPost(question));    // 重置输入框  this.setState({    questionInput: ‘‘  });    store.dispatch(receivePost());}

更常见的是第二种写法,一般会配合redux-action中间件使用。

redux-action中createAction的用法:

12345678
const a = createAction(‘test1‘, () => 10);a(); // {type: "test1", payload: 10}const b = createAction(‘test2‘);b(100); // {type: "test2", payload: 100}

使用redux-action将上面的写法改为:

1234567891011121314151617181920212223
// 使用redux-promise中间件解决异步操作第二种写法sendQuestion() {  const question = this.state.questionInput;  // Action Creator1  const requestPost = (question) => ({type: ‘SEND_QUESTION‘, status: ‘sending...‘, question});  // 发出同步Action  store.dispatch(requestPost(question));  // 重置输入框  this.setState({    questionInput: ‘‘  });  // 发出异步Action  store.dispatch(    createAction(‘RECEIVE_ANSWER‘)(      // Promise的then函数返回值才是createAction的第二个参数      Request.demo2.getAnswer({question}).then(v => ({          status: ‘‘,          answer: v        })      )    )  );}

注意,createAction的第二个参数实际上就是向要发送的Action的payload属性值,这里必须是一个Promise对象。(在reducer里面也必须从action.payload属性中获取对应的值)

明显,使用redux-promise的代码量更小一些,但是也因此失去了一定的灵活度,它的同步Action是脱离在异步操作之外单独存在的(即无法在一个Action Creator完成多个dispatch动作)

其他的比较热门的解决方案还有redux-promise-middleware(感觉像是前两者的一个集合)、redux-action-toolsredux-saga,可以学习这篇文章的讲解。

总结

学习Redux的异步操作和中间件之后,最大的体会就是太繁琐了,各种解决方案太多了。如果是复杂的项目中,有着复杂的业务逻辑,使用Redux会是一个很麻烦的事情。

以前在做一个React项目时,项目组选型使用的Mobx,当时没觉得有好用(当然也有用的比较浅的原因),但是仅仅是学习Redux,就发现Mobx或者是Vuex真的比Redux好上手太多了,Redux的函数式编程的思想带来的难度不仅是阅读、学习的难度,更是过多的范式代码带来的苦恼。

我认为会经久流传的解决方案一定会在可阅读性、可维护性以及入手难度上取得一个比较好的平衡,除非它是为了解决一些别人无法解决的问题而提出的,是一个时间段内近乎唯一的解决方案,但我感觉Redux好像并不是这样。

参考