前言
上一篇文章写了关于redux
的作用以及redux
和react-redux
两个插件的API,但redux
中有一个API:applyMiddleware
并没有说明,因为涉及到redux
的中间件概念,需要比较多内容去说明,这次这篇文章就集中写一下这方面的知识。
何为redux
中间件
1. 中间件的作用
之前我们说过,redux
的工作流程图是下面这样的:
我们看express
有中间件机制,其实redux
也有,redux
的中间件middleware
是用来增强dispatch
方法的。有时候当我们想改变dispatch
执行同时,也执行某些操作,例如日志记录,就可以用中间件实现该需求。如果我们把中间件也纳入到redux
的工作流程图,那新的流程图如下所示:
2. 用到中间件的简单例子
我们可以拿一个例子来说一下中间件,在上一篇文章中,我们写了一个计数和单位切换的例子,现在拿这个例子再添加一个需求,我希望可以从控制台里知道页面程序调用了哪些action
。虽然可以在每个action creator
都写打印输出语句,可是这不是最优解,我可以通过插入中间件来达到这个需求:
目录如下所示:
新增store/middleware/logger.js文件,内容如下:
// 中间件用函数来定义
const logger = store => next => action => {
console.info('dispatching', action.type)
next(action)
}
export default logger
index.js
import { createStore,applyMiddleware } from 'redux'
import reducer from './reducer'
import logger from './middleware/logger'
// createStore的第三个参数是用来定义中间件的,如果initalState(即下面的第二个参数)省略,则可以放在第二个参数的位置传进去
const store = createStore(reducer,{number:3,unit:'mm'},applyMiddleware(logger))
export default store
达到的效果如下所示:
项目代码
3. 中间件的使用方式
从上面的例子可知,中间件以函数来定义,其格式为:
store => next => action => {
// do something
}
在{}
里面需要调用next(action)
,不然后面的middleware
们不会处理该action
以及真正触发dispatch(action)
。
最后在生成Redux store
时作为第二或第三次参数传入到createStore
中,传入之前要用applyMiddleware
处理。下面来通过分析相关源码来了解下为什么要这么用:
4. Redux源码中是如何实现中间件的
createStore
我们先了解createStore
方法:
createStore(reducer, [preloadedState], enhancer)
该方法传入2~3个参数,最后会返回一个Redux store
。applyMiddleware(middle)
是作为enhancer
传入的,enhancer
是什么?下面先引用官方的解释对其说明:
总结以上的引用,其实enhancer
是一个用于更改增强Redux store
的函数,如何增强?我们先了解下createStore
函数的部分代码:
function createStore(
reducer,
preloadedState,
enhancer
) {
// ...无关代码不展示
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error(
`Expected the enhancer to be a function. Instead, received: '${kindOf(
enhancer
)}'`
)
}
return enhancer(createStore)(
reducer,
preloadedState
)
}
//... 一堆定义store函数的逻辑不展示
const store = {
dispatch: dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
return store
}
从createStore
函数的返回结果得知,store
本质上是一个带dispatch
,subscribe
,getState
,replaceReducer
以及$$observable
五个属性的普通object
对象。而当调用createStore
时有传入enhancer
,他会直接返回enhancer(createStore)(reducer,preloadedState)
,那其实enhancer(createStore)(reducer,preloadedState)
执行完成后最终返回的也是一个store
,我们可以推断enhancer
的编写格式是这样的:
(createStore)=>(reducer,preloadedState)=>{return store}。接下来我们看一下生成enhancer
的applyMiddleware
函数是怎样子的:
function applyMiddleware(...middlewares){
return (createStore) =>
(
reducer,
preloadedState
) => {
const store = createStore(reducer, preloadedState)
// 调用applyMiddleware时不允许middlewares为空
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
/**
* 通过compose形成调用链
* compose函数代码:
function compose(...funcs: Function[]) {
if (funcs.length === 0) {
return (arg) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce(
(a, b) =>
(...args: any) =>
a(b(...args))
)
}
*/
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
// 通过扩展运算符拆开store后合并成新的对象以更改dispatch方法
return {
...store,
dispatch
}
}
}
重点说一下这两行代码
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
之前说过,中间件的编写格式为store => next => action => {// do something},对照上面的代码来分析,假设我们按照以上的编写格式写了两个中间件分别是middleware1
和middleware2
如下所示:
const middleware1 = store => next => async(action) => {
console.info('middleware1 start')
await next(action)
console.info('middleware1 end')
}
const middleware2 = store => next => async(action) => {
console.info('middleware2 start')
await next(action)
console.info('middleware2 end')
}
当调用applyMiddleware(middleware1,middleware2)
传入这两个中间件,applyMiddleware
内部执行到const chain = middlewares.map(middleware => middleware(middlewareAPI))
这一条语句时,middlewareAPI
对应编写格式中的store
形参,返回的chain
是一个数组,其中的元素为 next => action => {// do something} 格式的函数,即是一个描述如何调用dispatch
的函数(next
是一个经包装或者原始的dispatch
,通过next(action)
可以派发action
)。
轮到下一条语句dispatch = compose(...chain)(store.dispatch)
,当执行compose(...chain)
时,根据注释中compose
函数的源码我们可以推断该语句执行后返回的结果为: (...args: any) =>chain1(chain2(...args))
,最终把store.dispatch
作为形参传入该函数时,相当于执行chain1(chain2(store.dispatch))
,会有下图的执行过程:
首先执行chain2
函数,store.dispatch
作为chain2
中的next
新参传入,chain2
立即返回一个格式为action=>{} 的函数,该函数作为chain1
中的next
新参传入chain1
中,而chain1
也会返回一个格式一样为action=>{} 的函数赋值给dispatch
。该dispatch
会在applyMiddleware
函数中最后的语句return {...store,dispatch}
与store
合并返回出去。以上过程中,chain1
和chain2
返回的action=>{}
的函数都以闭包的方式记录着next
变量。
当在开发代码中dispatch(action)
被调用时,会呈现以下的调用流程:
因dispatch
指向chain1
,故先执行chain1
,执行到next(action)
语句时,其next
指向chain2
,故开始执行chain2
,执行到next(action)
语句时,next
指向store
原始的dispatch
方法,从而实现了增强dispatch
方法。上面的调用流程中控制台的输出会是以下的结果:
middleware1 start
middleware2 start
middleware2 end
middleware1 end
关于异步action
存在以下需求,我需要把github
中的表情包数据放到Redux store
中供项目里的多个模块使用,而这些数据需要异步请求获取,这时候我们遇到一个难题,因reducer
原则上是纯函数,因此,异步操作这类不纯的行为不能出现在reducer
中,针对此问题,我们可以绕个弯子,写个如下的公共函数,获取响应后调用dispatch
设置状态,下面我来写一个例子来实践一下上述思路:
utils\index.js
import store from '../store'
import {SET_EMOJIS} from '../store/action'
// 公共函数,用于请求或更新表情图数据
export function requestEmojis(){
fetch('https://api.github.com/emojis') // 数据从github的公共开放接口获取
.then(res=>res.json())
.then(emojis=>store.dispatch(SET_EMOJIS(emojis)))
}
下面是store
的代码:
store\index.js
import { createStore } from 'redux'
import reducer from './reducer'
// 把数据初始值设为对象
const store = createStore(reducer,{})
export default store
store\action\index.js
// 用于生成设置表情图数据的action的action creator
export const SET_EMOJIS=(emojis)=>({
type:'SET_EMOJIS',
emojis
})
store\reducer\index.js
const reducer = (state,action)=>{
switch (action.type) {
case 'SET_EMOJIS':
return action.emojis
default:
return state
}
}
export default reducer
最后我们来通过以下组件查看效果:
App.jsx
import React from 'react';
import { connect } from 'react-redux'
import {requestEmojis} from '../utils'
const App = (props)=>{
const {emojis} = props
return <div>
<h2>emojis</h2>
// 点击该按钮后通过调用公共方法requestEmojis获取表情图并存到Redux store中
<button onClick={requestEmojis}>获取emojis</button><br/>
{
Object.entries(emojis)
.slice(0,50) // 数据有点多,所以只显示50个表情图
.map(([key,value])=>
<img src={value} alt={key} title={key} key={key}/>
)
}
</div>
}
const mapStateToProps = (state) => ({
emojis:state
})
export default connect(mapStateToProps,null)(App)
最后我们来看一下效果:
项目地址
但在实际开发中,这种做法并不常用,原因可以等我介绍了redux-thunk
的用法后,再拿这两种用法分析对比。
我们更偏向于利用第三方插件实现异步action,异步action指指向异步操作的action
。下面我们来依次看一下上面所说到的常用的第三方插件redux-thunk
和redux-promise
:
redux-thunk
使用方法
我们在上面的例子引入redux-thunk
进行改造,在调用createStore
创建Redux store
时,就要通过applyMiddleware
加载redux-thunk
,如下所示:
store\index.js
import { createStore,applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'
const store = createStore(reducer,{}, applyMiddleware(thunk))
export default store
然后我们在 store\action\index.js 中加一个异步action
如下所示: (注意此处的action
是一个函数,而并非是以往的带type
属性的纯对象):
store\action\index.js
export const SET_EMOJIS=(emojis)=>({
type:'SET_EMOJIS',
emojis
})
// 此处的异步action为一个高阶函数,返回结果也是一个函数
// 此处的REQUEST_EMOJIS也是一个Action Creator,所谓Action Creator指创建异步action或同步action的函数
export const REQUEST_EMOJIS = ()=>dispatch => (
fetch('https://api.github.com/emojis')
.then((res)=>res.json())
.then(emojis => dispatch(SET_EMOJIS(emojis)))
)
最后更改一下App.jsx
App.jsx
import React from 'react';
import { connect } from 'react-redux'
import {REQUEST_EMOJIS} from '../store/action/index'
const App = (props)=>{
const {emojis} = props
return <div>
<h2>emojis</h2>
<button onClick={props.requestEmojis}>获取表情图</button>
<br/>
{
Object.entries(emojis).slice(0,50).map(([key,value])=>
<img src={value} alt={key} title={key} key={key}/>
)
}
</div>
}
const mapStateToProps = (state) => ({
emojis:state
})
const mapDispatchToProps = (dispatch) => ({
requestEmojis: () => dispatch(REQUEST_EMOJIS()),
})
export default connect(mapStateToProps,mapDispatchToProps)(App)
这样子就可以不调用异步请求的公共函数的同时也实现上面的效果,项目地址。
值得注意的是,被dispatch
派发的 异步action
是一个函数,格式是(dispatch, getState, extraArgument)=>{}
。
源码分析
现在来分析一下redux-thunk
的源码,源码非常简洁,如下所示:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
// 如果传入的action是一个函数,则代表该action为异步action,则把dispatch, getState, extraArgument作为形参传入该异步action执行
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
上面的代码太精简了,我觉得我都不用解释什么了,不过从源码中我们可以看出一点,在使用redux-thunk
时,异步action
必须写成(dispatch, getState, extraArgument)=>{}
格式,然后在执行过程中需要调用dispatch(action)
派发。
拓展:为什么要用redux-thunk(此章节可跳过)
此章节可能跟文章无关,我是兴趣之余写的,可以直接跳过
为什么目前大多数用的是redux-thunk
而不是像开头的异步公共函数的方式去解决异步操作。我从stackoverflow中的问题how-to-dispatch-a-redux-action-with-a-timeout其中Dan Abramov(Redux作者) 的回答中得出了主要的答案:
对比于redux-thunk
,使用异步公共函数的方式会导致:
-
不利于服务端渲染
答案中是这么写的:
在Redux关于服务端渲染的链接Redux Server Rendering中,这里 我们可以知道,每一次请求经服务端渲染的页面时,后端都会:
- 创建一个新的
Redux store
,选择性地派发部分action
, - 然后模板页面可能某些占位符用
Redux store
中state
的数据填充 - 从
Redux store
获取state
,然后在和已渲染的HTML
放到响应信息中一并传到客户端。客户端会根据响应的state
创建Redux store
。
在上面采用异步公共函数的方式方案的例子中,
store
出现在两个地方,一处是<Provider store={store}>
中,一处是requestEmojis
公共函数中,在服务端渲染中如果调用到requestEmojis
,那需要保证两个地方的store
是同一个实例。这样子会增加后端代码的复杂度。但是如果使用redux-thunk
,那store
只出现在<Provider store={store}>
中,我们不需要考虑保证单例的问题。 - 创建一个新的
-
不利于测试代码的编写
引用答案中的描述:
在保证上述所说的单例时,我们会很难编写测试用例,因为对于
requestEmojis
公共函数的测试中,其调用的store
是一个真正的Redux store
,其duspatch
的调用会影响到页面的显示,因此,我们不能通过jest
里bypassing-module-mocks中的jest.mock
去取替这个store
。Redux
不推荐手写Action Creator
,他们更推荐使用@reduxjs/toolkit去生成Action Creator
。更详细的资料可参考action-creators--thunks。 -
难以区分容器组件和展示组件
什么是容器组件(container components) 和 展示组件(presentational components),我引用别的文章的一张图来解释:
图片来源:blog.csdn.net/weixin_4604…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!