Redux02 异步操作和中间件
学习Redux中间件的概念,以及使用Redux中间件完成异步操作的方法。
同步和异步流程
先来复习一下Redux的基本流程:
1 | 1. 用户发出Action |
在第1、2步之间有一个问题,之前考虑的情况都是在Action发出之后,Reducer立刻计算出State,这是一个同步的过程。如果在Action发出之后,过一段时间再执行Reducer,这是异步过程:
1 | 1. 用户发出Action |
现在我们希望的是在异步操作结束后,自动执行Reducer,这就要用到中间件(middleware)
中间件的概念
什么是中间件?中间件(middleware)是一种很常见、也很强大的模式,被广泛应用在Express、Koa、Redux等类库和框架当中。
简单来说,中间件就是在调用目标函数之前,可以随意插入其他函数预先对数据进行处理、过滤,在这个过程里面你可以打印数据、或者停止往下执行中间件等。数据就像水流一样经过中间件的层层的处理、过滤,最终到达目标函数。
1 | // 中间件可以把 A 发送数据到 B 的形式从 |
具体到Redux来看,如果要实现中间件,最合适环节就是在发送Action的环节,即使用中间件包裹store.dispatch
来添加功能,比如要增加打印功能,将Action和State打印出来,我们就可以编写这样一个中间件:
1 | const next = store.dispatch; |
中间件对store.dispatch
进行了改造,在发出Action和执行Reducer之间添加了其他功能。但是实际上中间件的写法不是这样的。
在Redux中,中间件是纯函数,有明确的使用方法,并且要严格的遵循以下格式:
1 | var anyMiddleware = function ({ dispatch, getState }) { |
中间件由三个嵌套的函数构成(会依次调用):
(1)第一层向其余两层提供分发函数dispatch
和getState
函数
(2)第二层提供next
函数,它允许你显示的将处理过的输入传递给下一个中间件或Redux(这样Redux才能调用所有reducer)。实际上next
作为参数,就是通过componse
传入的下一个要执行的函数,通过next(action)
就将action
传递给了下一中间件
(3)第三层提供从上一个中间件或者从dispatch
传递过来的Action,这个Action可以调用下一个中间件(让Action继续流动)或者以想要的方式处理action
所以一个Log的中间件应该这样写:
1 | function logMiddleware ({ dispatch, getState }) { |
next(action)
就是继续传递Action,如果不进行这一步,所有的Action都会被丢弃。
中间件的用法
常用的中间件都有现成的,不用我们自行编写,只需要直接引用别人写好的模块即可,比如上面的打印日志的中间件,就可以使用现成的redux-logger模块:
1 | import { applyMiddleware, createStore } from 'redux'; |
使用的时候首先通过redux-logger提供的生成方法createLogger
创建一个中间件实例logger
,然后将它放在Redux提供的applyMiddleware
方法中,放到createStore
方法中(由于createStore
方法可以接受应用的初始状态作为第二个参数,这个时候applyMiddleware
方法就是第三个参数了)
有的中间件有次序要求,必须放在何时的位置才能正确输出,使用之前要查看文档。
applyMiddleware()
applyMiddleware()
是Redux的原生方法,会将所有中间件组成一个数组,依次执行,下面是它的源码:
1 | export default function applyMiddleware(...middlewares) { |
applyMiddleware
可以接受多个中间件作为参数,全部放进了数组chain
中,每个中间件接受Store的dispatch
和getState
函数作为命名参数,返回一个函数。该函数会被传入称为next
的下一个中间件的dispatch
方法,并返回一个接受Action的新函数,这个函数可以直接调用next(action)
。这个过程是通过compose
方法完成的。
多个中间件形成了一个调用链,调用链中的最后一个中间件会接受真实Store的dispatch
作为next
参数,并借此结束调用链。
1 | // 中间件函数的函数签名 |
compose()
compose(...functions)
的功能是从右到左来组合多个函数,这是函数式编程的方法,其中每个函数的返回值作为参数提供给左边的函数:
1 | compose(funcA, funcB, funcC); |
关于compose
方法,以前做过一道练习题《前端练习17 函数式编程的compose函数》,手写简易的compose
方法。
1 | const store = createStore( |
异步操作的基本思路
处理异步操作需要使用中间件。
同步操作只要发出一种Action即可,异步操作的差别是要发出三种Action
1 | - 操作发起时的Action |
以向服务器取出数据为例,三种Action有两种不同的写法:
1 | // 写法一,名称相同,参数不同 |
除了Action种类不同,异步操作的State也要进行改造,反映不同的操作状态,例如:
1 | const state = { |
State中的属性isFetching
表示是否正在抓取数据,didInvalidate
表示是否正过期,lastUpdated
表示上一次更新事件。
现在整个异步操作的思路就很清晰了:
1 | 1. 操作开始,发出一个Action,触发State更新为“正在操作”状态,View重新渲染 |
redux-thunk中间件
异步操作至少要发出两个Action,用户操作触发第一个Action,这个和同步操作一样,标识着异步操作的开始,现在要做的是在异步操作结束时,自动发送第二个Action
奥妙就在Action Creator中,需要对其进行改造。我们有一个组件,点击按钮后会发出一个Ajax请求,将返回的结果填充在视图中,按钮的点击事件如下:
1 | sendQuestion() { |
其中最关键的就是actionCreator
,它的返回值是一个函数,这个函数执行时,会先发出一个ActionrequestPost
(由Action Creator生成)并进行其他同步操作,然后进行异步操作Request.demo2.getAnswer({ question })
,在异步操作的回调函数中发出第二个ActionactionCreator
(由Action Creator2生成)。
上面的代码中,有几点要注意:
(1)完成异步操作的Action CreatoractionCreator
返回的是一个函数,普通的Action Creator返回的是Action对象
(2)返回的这个函数参数是dispatch
和getState
这两个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返回的函数时就传入了dispatch
和getState
两个参数
1 | import { createStore, applyMiddleware } from "redux"; |
因此,异步操作的第一种解决方案就是,==编写一个返回函数的Action Creator,然后使用redux-thunk中间件改造store.dispatch
==
redux-promise中间件
在上面的Action Creator返回了一个函数,也可以返回其他值,另一种异步操作的解决方案,就是让Action Creator返回一个Promise对象
这需要使用redux-promise中间件
1 | import { createStore, applyMiddleware } from "redux"; |
来看一下它的源码:
1 | import isPromise from 'is-promise'; |
如果Action本身是一个Promise,它resolve后的值是一个Action对象,会被dispatch
方法送出,reject
后不会有任何动作,如果Action本身不是一个Promise对象,而Action对象的payload
属性是一个Promise对象,那么无论其resolve或reject,dispatch
都会发出Action
所以有两种写法,一种是让Action本身返回一个Promise对象:
1 | sendQuestion() { |
更常见的是第二种写法,一般会配合redux-action中间件使用。
redux-action中createAction
的用法:
1 | const a = createAction('test1', () => 10); |
使用redux-action将上面的写法改为:
1 | // 使用redux-promise中间件解决异步操作第二种写法 |
注意,createAction的第二个参数实际上就是向要发送的Action的payload
属性值,这里必须是一个Promise对象。(在reducer里面也必须从action.payload
属性中获取对应的值)
明显,使用redux-promise的代码量更小一些,但是也因此失去了一定的灵活度,它的同步Action是脱离在异步操作之外单独存在的(即无法在一个Action Creator完成多个dispatch
动作)
其他的比较热门的解决方案还有redux-promise-middleware(感觉像是前两者的一个集合)、redux-action-tools、redux-saga,可以学习这篇文章的讲解。
总结
学习Redux的异步操作和中间件之后,最大的体会就是太繁琐了,各种解决方案太多了。如果是复杂的项目中,有着复杂的业务逻辑,使用Redux会是一个很麻烦的事情。
以前在做一个React项目时,项目组选型使用的Mobx,当时没觉得有好用(当然也有用的比较浅的原因),但是仅仅是学习Redux,就发现Mobx或者是Vuex真的比Redux好上手太多了,Redux的函数式编程的思想带来的难度不仅是阅读、学习的难度,更是过多的范式代码带来的苦恼。
我认为会经久流传的解决方案一定会在可阅读性、可维护性以及入手难度上取得一个比较好的平衡,除非它是为了解决一些别人无法解决的问题而提出的,是一个时间段内近乎唯一的解决方案,但我感觉Redux好像并不是这样。