再探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如果有疑惑,請看上面:point_up_2: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正是中介軟體的加持,才變得越發強大。也希望我們每天能進步一點點,造就更美好的自己。