再探Redux Middleware
前言
在初步了解Redux中间件演变过程之后,继续研究Redux如何将中间件结合。上次将中间件与redux硬结合在一起确实有些难看,现在就一起看看Redux如何加持中间件。
- 中间件执行过程
希望借助图形能帮助各位更好的理解中间件的执行情况。
- redux如何加持中间件
现在是时候看看redux是如何将中间件结合了,我们在源码中一探究竟。
* @param {Function} [enhancer] The store enhancer. You may optionally specify it * to enhance the store with third-party capabilities such as middleware, * time travel, persistence, etc. The only store enhancer that ships with Redux * is `applyMiddleware()`. * * @returns {Store} A Redux store that lets you read the state, dispatch actions * and subscribe to changes. */ export default function createStore(reducer, preloadedState, enhancer) { if ( (typeof preloadedState === 'function' && typeof enhancer === 'function') || (typeof enhancer === 'function' && typeof arguments[3] === 'function') ) { throw new Error( 'It looks like you are passing several store enhancers to ' + 'createStore(). This is not supported. Instead, compose them ' + 'together to a single function' ) } if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState // 如果初始化state是一个函数,则认为有中间件 preloadedState = undefined } if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) }
如果createStore第二个参数是函数(第二,第三都是函数会抛异常),则redux认为第二个参数是调用applyMiddleware函数的返回值(注释有说明)。
根据return enhancer(createStore)(reducer, preloadedState),说明applyMiddleware返回了一个函数,该函数内还返回了一个函数。那么接下来从applyMiddleware源码中一探究竟。
export default function applyMiddleware(...middlewares) { // 将所有中间件存入middlewares数组 return createStore => (...args) => { // 返回函数以createStore为参数,args即[reducer, preloadedState] const store = createStore(...args) // 创建一个store let dispatch = () => { // 定义一个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: (...args) => dispatch(...args) // middlewareAPI的dispatch属性指向一个匿名函数,该函数内部会执行外部dispatch变量指向的那个函数。 } const chain = middlewares.map(middleware => middleware(middlewareAPI)) // 执行每个中间件,顺带检查是否有中间件调用传入参数中的dispatch,如果有则抛出异常 dispatch = compose(...chain)(store.dispatch) // 将chain展开传入compose,然后执行返回的函数,传入store.dispatch,最后将所有中间件组合成最终的中间件,并将dispatch变量指向这个中间件。 // 由于dispatch变量的更改,它原来指向的匿名函数现在没有任何变量指向它,会被垃圾回收。
// 误区:调用middlewareAPI的dispatch属性指向的函数时,内部的dispatch会指向原来抛出异常的匿名函数。这是错误的,在调用middlewareAPI的dispatch属性所指向的函数时,
// 会寻找dispatch变量,函数内部找不到就向外部作用域寻找,然后找到外部dispatch,而此时外部的dispatch指向最终的中间件,所以会调用最终的中间件。这对于理解redux-thunk非常重要。
return { ...store, dispatch // 覆盖store中dispatch变量 } } }
上面的代码中还有一点疑惑,compose函数是什么样子,那么我们再探compose。
* @param {...Function} funcs The functions to compose. * @returns {Function} A function obtained by composing the argument functions * from right to left. For example, compose(f, g, h) is identical to doing * (...args) => f(g(h(...args))). 可以发现,和我们之前写的代码效果一模一样 */ export default function compose(...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args) => a(b(...args))) }
也许你对数组的reduce方法不是很熟,上篇文章篇幅也比较饱满。那么这儿简单讲解下:
[1, 2, 3, 4].reduce((a, b) => { console.log(a, b); return a + b }) // 1 2 可以发现第一次执行,我们拿到数组的第1,2个变量 // 3 3 拿到上次返回的结果和第3个变量 // 6 4 拿到上次返回的结果和第4个变量
最后结果为10,没有打印所以看不出。当然数组存储的也可能是对象,在reduce函数执行时,拿到每个变量的副本(浅拷贝),然后根据你的代码做对应的事。在这就以上篇文章的中间
件为例,再加入logMiddleware3(和logMiddleware2类似,只是将打印的数字部分改为3而已),看看compose函数执行过程。
[logMiddleware3, logMiddleware2, logMiddleware].reduce((a, b) => (...args) => a(b(...args)))
// 假定compose函数传入的参数为store.dispatch,则有以下结果:
// (logMiddleware3, logMiddleware2) => (...args) => logMiddleware3(logMiddleware2(...args)) 这里args[0]为logMiddleware(store.dispatch)返回的中间件
// (logMiddleware3(logMiddleware2(...args)), logMiddleware) => (...args) => logMiddleware3(logMiddleware2(logMiddleware(...args))) 这里的args[0]为store.dispatch
// 最后返回(...args)=> logMiddleware3(logMiddleware2(logMiddleware(...args))) ,接着执行该函数,传入store.dispatch,也就产生了最终的中间件
现在对于redux结合过程已经有了一定的认识,是时候看看别人的中间件了,对比我们自己的中间件,也许有不同的收获。
- redux-thunk
至此我们写的中间件都比较好理解,是时候认识下redux-thunk了。它又会有什么特别之处了,让我们一起看看源码。
function createThunkMiddleware(extraArgument) { // 这里extraArgument完全没用到 return ({ dispatch, getState }) => next => action => { // 这里的dispatch如果有疑惑,请看上面👆applyMiddleware源码解析 if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
what?太精辟了有木有。其实理解起来也很简单,如果传入的action是一个函数,则调用该函数;否则调用上一个中间件并返回结果。当然你还可以再精简些。
return typeof action === 'function' ? action(dispatch, getState, extraArgument) : next(action)
那么问题来了,什么时候用得着redux-thunk呢?也就是什么情况下action会是函数。我们仔细看看action为函数时,它的参数也就明白了。在执行action函数时,我们还能调用dispatch,说明dispatch操作是要等待某个东西执行完才可以执行。说到这,还能是什么呢?当然非异步任务莫属了。
好了现在我们将原来的代码更改下,实现和redux,redux-thunk结合,这里我们先自己实现redux-thunk。
function ThunkMiddleware() { return ({ dispatch, getState }) => next => action => { return typeof action === 'function' ? action(dispatch, getState) : next(action) } } const thunk = ThunkMiddleware(); export default thunk;
新建middleware目录,新建redux-thunk和redux-logger,接着封装redux-logger模块。
function LoggerMiddleware() { return ({ getState }) => next => action => { console.log('dispatch: ', action); let result = next(action); console.log('nextState: ', getState()); return result; } } const logger = LoggerMiddleware(); export default logger;
更改index.js。
import React from 'react'; import ReactDOM from 'react-dom'; import { createStore, applyMiddleware } from 'redux'; import { Provider } from 'react-redux'; import App from './App'; import * as serviceWorker from './serviceWorker'; import reduxLogger from './middlewares/redux-logger'; import reduxThunk from './middlewares/redux-thunk'; function listReducer(state = { list: [] }, action) { switch (action.type) { case 'receive': return { list: action.data }; default: return state; } } const store = createStore(listReducer, applyMiddleware(reduxLogger, reduxThunk)); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root')); serviceWorker.unregister();
更改App.js。
import React, { Component } from 'react'; import { connect } from 'react-redux'; import axios from 'axios'; import Mock from 'mockjs'; Mock.mock('http://test.com/search', { 'list|0-5': [{ 'id|+1': 1, name: '@character("lower")', 'version': '@float(1, 10, 2, 2)', publisher: '@cname' }] }); class App extends Component { state = { searchValue: '', }; handleSearch = e => { e.preventDefault(); if (this.state.searchValue) { this.props.changeList(this.state.searchValue); } }; changeValue = e => { this.setState({ searchValue: e.target.value }); }; render() { return ( <div style={{ textAlign: 'center', margin: '40px' }}> <form onSubmit={this.handleSearch}> <input type="text" value={this.state.searchValue} onChange={this.changeValue} /> <button type="submit">搜索</button> </form> <ul> {this.props.list.map(item => ( <li key={item.id} style={{ listStyle: 'none' }}> <p>{item.name}</p> <p> {item.publisher} publish {item.version} </p> </li> ))} </ul> </div> ); } } const fetchResult = (searchValue) => dispatch => { return axios.get(`http://test.com/search`).then(result => { if (result.status === 200) { const data = result.data.list.map(item => ({...item, name: `${searchValue}${item.name}`})); const action = { type: 'receive', data }; dispatch(action); } }) }; function mapStateToProps(state) { return { list: state.list } } function mapDispatchToProps(dispatch) { return { changeList: searchValue => dispatch(fetchResult(searchValue)) } } export default connect(mapStateToProps, mapDispatchToProps)(App);
项目目录如下:
- 结语
如果你理解了redux处理中间件的过程,那本文的目的也达到了。海纳百川,有容乃大。redux正是中间件的加持,才变得越发强大。也希望我们每天能进步一点点,造就更美好的自己。