Redux/react-redux/redux中間件設計實現剖析

2021-12-17 全棧前端精選

其實筆者本來沒有redux相關的行文計劃,不過公司內部最近有同事作了redux相關的技術分享,而筆者承擔了一部分文章評審的任務,在評審的過程中,筆者花了相當的精力時間來查閱資料和實現代碼,前後積攢了幾千字的筆記,對redux也有了一份心得見解,於是順手寫就本文,希望能給大家帶來些一些啟發和思考Thanks♪(・ω·)ノ經過本文的學習,讀者應該能夠學習理解:

❞redux中间件的設計思路及實現原理一. redux的實現

在一切開始之前,我們首先要回答一個問題:為什麼我們需要redux,redux為我們解決了什麼問題?只有回答了這個問題,我們才能把握redux的設計思路。

React作為一個組件化開發框架,組件之間存在大量通信,有時這些通信跨越多個組件,或者多個組件之間共享一套數據,簡單的父子組件間傳值不能滿足我們的需求,自然而然地,我們需要有一個地方存取和操作這些公共狀態。而redux就為我們提供了一種管理公共狀態的方案,我們後續的設計實現也將圍繞這個需求來展開。

我們思考一下如何管理公共狀態:既然是公共狀態,那麼就直接把公共狀態提取出來好了。我們創建一個store.js文件,然後直接在裡邊存放公共的state,其他組件只要引入這個store就可以存取共用狀態了。

const state = {    
    count: 0
}

我們在store裡存放一個公共狀態count,組件在import了store後就可以操作這個count。這是最直接的store,當然我們的store肯定不能這麼設計,原因主要是兩點:

「1. 容易誤操作」

比如說,有人一個不小心把store賦值了{},清空了store,或者誤修改了其他組件的數據,那顯然不太安全,出錯了也很難排查,因此我們需要「有條件地」操作store,防止使用者直接修改store的數據。

「2. 可讀性很差」

JS是一門極其依賴語義化的語言,試想如果在代碼中不經注釋直接修改了公用的state,以後其他人維護代碼得多懵逼,為了搞清楚修改state的含義還得根據上下文推斷,所以我們最好是給每個操作「起個名字」

項目交接

我們重新思考一下如何設計這個「公共狀態管理器」,根據我們上面的分析,我們希望公共狀態既能夠被全局訪問到,又是私有的不能被直接修改,思考一下,「閉包」是不是就就正好符合這兩條要求,因此我們會把公共狀態設計成閉包(對閉包理解有困難的同學也可以跳過閉包,這並不影響後續理解)

既然我們要存取狀態,那麼肯定要有「getter」「setter」,此外當狀態發生改變時,我們得進行廣播,通知組件狀態發生了變更。這不就和redux的三個API:getState、dispatch、subscribe對應上了嗎。我們用幾句代碼勾勒出store的大致形狀:

export const createStore = () => {    
    let currentState = {}       // 公共狀態    
    function getState() {}      // getter    
    function dispatch() {}      // setter    
    function subscribe() {}     // 發布訂閱    
    return { getState, dispatch, subscribe }
}

1. getState實現

getState()的實現非常簡單,返回當前狀態即可:

export const createStore = () => {    
    let currentState = {}       // 公共狀態    
    function getState() {       // getter        
        return currentState    
    }    
    function dispatch() {}      // setter    
    function subscribe() {}     // 發布訂閱    
    return { getState, dispatch, subscribe }
}

2.dispatch實現

但是dispatch()的實現我們得思考一下,經過上面的分析,我們的目標是「有條件地、具名地」修改store的數據,那麼我們要如何實現這兩點呢?我們已經知道,在使用dispatch的時候,我們會給dispatch()傳入一個action對象,這個對象包括我們要修改的state以及這個操作的名字(actionType),根據type的不同,store會修改對應的state。我們這裡也沿用這種設計:

export const createStore = () => {    
    let currentState = {}    
    function getState() {        
        return currentState    
    }    
    function dispatch(action) {        
        switch (action.type) {            
            case 'plus':            
            currentState = {                 
                ...state,                 
                count: currentState.count + 1            
            }        
        }    
    }    
    function subscribe() {}    
    return { getState, subscribe, dispatch }
}

我們把對actionType的判斷寫在了dispatch中,這樣顯得很臃腫,也很笨拙,於是我們想到把這部分修改state的規則抽離出來放到外面,這就是我們熟悉的**reducer。**我們修改一下代碼,讓reducer從外部傳入:

import { reducer } from './reducer'
export const createStore = (reducer) => {    
    let currentState = {}     
    function getState() {        
        return currentState    
    }    
    function dispatch(action) {         
        currentState = reducer(currentState, action)  
    }    
    function subscribe() {}    
    return { getState, dispatch, subscribe }
}

然後我們創建一個reducer.js文件,寫我們的reducer


//reducer.js
const initialState = {    
    count: 0
}
export function reducer(state = initialState, action) {    
    switch(action.type) {      
        case 'plus':        
        return {            
            ...state,                    
            count: state.count + 1        
        }      
        case 'subtract':        
        return {            
            ...state,            
            count: state.count - 1        
        }      
        default:        
        return initialState    
    }
}

代碼寫到這裡,我們可以驗證一下getState和dispatch:

//store.js
import { reducer } from './reducer'
export const createStore = (reducer) => {    
    let currentState = {}        
    function getState() {                
        return currentState        
    }        
    function dispatch(action) {                
        currentState = reducer(currentState, action)  
    }        
    function subscribe() {}        
    return { getState, subscribe, dispatch }
}

const store = createStore(reducer)  //創建store
store.dispatch({ type: 'plus' })    //執行加法操作,給count加1
console.log(store.getState())       //獲取state

運行代碼,我們會發現,列印得到的state是:{ count: NaN },這是由於store裡初始數據為空,state.count + 1實際上是underfind+1,輸出了NaN,所以我們得先進行store數據初始化,我們在執行dispatch({ type: 'plus' })之前先進行一次初始化的dispatch,這個dispatch的actionType可以隨便填,只要不和已有的type重複,讓reducer裡的switch能走到default去初始化store就行了:

import { reducer } from './reducer'
export const createStore = (reducer) => {        
    let currentState = {}        
    function getState() {                
        return currentState        
    }        
    function dispatch(action) {                
        currentState = reducer(currentState, action)        
    }        
    function subscribe() {}    
    dispatch({ type: '@@REDUX_INIT' })  //初始化store數據        
    return { getState, subscribe, dispatch }
}

const store = createStore(reducer)      //創建store
store.dispatch({ type: 'plus' })        //執行加法操作,給count加1
console.log(store.getState())           //獲取state

運行代碼,我們就能列印到的正確的state:{ count: 1 }

3.subscribe實現

儘管我們已經能夠存取公用state,但store的變化並不會直接引起視圖的更新,我們需要監聽store的變化,這裡我們應用一個設計模式——觀察者模式,觀察者模式被廣泛運用於監聽事件實現(有些地方寫的是發布訂閱模式,但我個人認為這裡稱為觀察者模式更準確,有關觀察者和發布訂閱的區別,討論有很多,讀者可以搜一下)

所謂觀察者模式,概念很簡單:觀察者訂閱被觀察者的變化,被觀察者發生改變時,通知所有的觀察者;也就是監聽了被觀察者的變化。那麼我們如何實現這種變化-通知的功能呢,為了照顧還不熟悉觀察者模式實現的同學,我們先跳出redux,寫一段簡單的觀察者模式實現代碼:

//觀察者
class Observer {    
    constructor (fn) {      
        this.update = fn    
    }
}
//被觀察者
class Subject {    
    constructor() {        
        this.observers = []          //觀察者隊列    
    }    
    addObserver(observer) {          
        this.observers.push(observer)//往觀察者隊列添加觀察者    
    }    
    notify() {                       //通知所有觀察者,實際上是把觀察者的update()都執行了一遍       
        this.observers.forEach(observer => {        
            observer.update()            //依次取出觀察者,並執行觀察者的update方法        
        })    
    }
}

var subject = new Subject()       //被觀察者
const update = () => {console.log('被觀察者發出通知')}  //收到廣播時要執行的方法
var ob1 = new Observer(update)    //觀察者1
var ob2 = new Observer(update)    //觀察者2
subject.addObserver(ob1)          //觀察者1訂閱subject的通知
subject.addObserver(ob2)          //觀察者2訂閱subject的通知
subject.notify()                  //發出廣播,執行所有觀察者的update方法

解釋一下上面的代碼:觀察者對象有一個update方法(收到通知後要執行的方法),我們想要在被觀察者發出通知後,執行該方法;被觀察者擁有addObserver和notify方法,addObserver用於收集觀察者,其實就是將觀察者們的update方法加入一個隊列,而當notify被執行的時候,就從隊列中取出所有觀察者的update方法並執行,這樣就實現了通知的功能。我們redux的發布訂閱功能也將按照這種實現思路來實現subscribe:

有了上面觀察者模式的例子,subscribe的實現應該很好理解,這裡把dispatch和notify做了合併,我們每次dispatch,都進行廣播,通知組件store的狀態發生了變更。

import { reducer } from './reducer'
export const createStore = (reducer) => {        
    let currentState = {}        
    let observers = []             //觀察者隊列        
    function getState() {                
        return currentState        
    }        
    function dispatch(action) {                
        currentState = reducer(currentState, action)                
        observers.forEach(fn => fn())        
    }        
    function subscribe(fn) {                
        observers.push(fn)        
    }        
    dispatch({ type: '@@REDUX_INIT' })  //初始化store數據        
    return { getState, subscribe, dispatch }
}

我們來試一下這個subscribe(這裡我就不創建組件再引入store再subscribe了,直接在store.js中模擬一下兩個組件使用subscribe訂閱store變化):

import { reducer } from './reducer'
export const createStore = (reducer) => {        
    let currentState = {}        
    let observers = []             //觀察者隊列        
    function getState() {                
        return currentState        
    }        
    function dispatch(action) {                
        currentState = reducer(currentState, action)                
        observers.forEach(fn => fn())        
    }        
    function subscribe(fn) {                
        observers.push(fn)        
    }            
    dispatch({ type: '@@REDUX_INIT' })  //初始化store數據        
    return { getState, subscribe, dispatch }
}

const store = createStore(reducer)       //創建store
store.subscribe(() => { console.log('組件1收到store的通知') })
store.subscribe(() => { console.log('組件2收到store的通知') })
store.dispatch({ type: 'plus' })         //執行dispatch,觸發store的通知

控制臺成功輸出store.subscribe()傳入的回調的執行結果:

執行結果

到這裡,一個簡單的redux就已經完成,在redux真正的源碼中還加入了入參校驗等細節,但總體思路和上面的基本相同。

我們已經可以在組件裡引入store進行狀態的存取以及訂閱store變化,數一下,正好十行代碼(`∀´)Ψ。但是我們看一眼右邊的進度條,就會發現事情並不簡單,篇幅到這裡才過了三分之一。儘管說我們已經實現了redux,但coder們並不滿足於此,我們在使用store時,需要在每個組件中引入store,然後getState,然後dispatch,還有subscribe,代碼比較冗餘,我們需要合併一些重複操作,而其中一種簡化合併的方案,就是我們熟悉的「react-redux」

二. react-redux的實現

上文我們說到,一個組件如果想從store存取公用狀態,需要進行四步操作:import引入store、getState獲取狀態、dispatch修改狀態、subscribe訂閱更新,代碼相對冗餘,我們想要合併一些重複的操作,而react-redux就提供了一種合併操作的方案:react-redux提供 Provider和 connect兩個API,Provider將store放進this.context裡,省去了import這一步,connect將getState、dispatch合併進了this.props,並自動訂閱更新,簡化了另外三步,下面我們來看一下如何實現這兩個API:

1. Provider實現

我們先從比較簡單的Provider開始實現,Provider是一個組件,接收store並放進全局的context對象,至於為什麼要放進context,後面我們實現connect的時候就會明白。下面我們創建Provider組件,並把store放進context裡,使用context這個API時有一些固定寫法(有關context的用法可以查看這篇文章)

import React from 'react'
import PropTypes from 'prop-types'
export class Provider extends React.Component {  
    // 需要聲明靜態屬性childContextTypes來指定context對象的屬性,是context的固定寫法  
    static childContextTypes = {    
        store: PropTypes.object  
    } 

    // 實現getChildContext方法,返回context對象,也是固定寫法  
    getChildContext() {    
        return { store: this.store }  
    }  

    constructor(props, context) {    
        super(props, context)    
        this.store = props.store  
    }  

    // 渲染被Provider包裹的組件  
    render() {    
        return this.props.children  
    }
}

完成Provider後,我們就能在組件中通過this.context.store這樣的形式取到store,不需要再單獨import store。

2. connect實現

下面我們來思考一下如何實現connect,我們先回顧一下connect的使用方法:

connect(mapStateToProps, mapDispatchToProps)(App)

我們已經知道,connect接收mapStateToProps、mapDispatchToProps兩個方法,然後返回一個高階函數,這個高階函數接收一個組件,返回一個高階組件(其實就是給傳入的組件增加一些屬性和功能)connect根據傳入的map,將state和dispatch(action)掛載子組件的props上,我們直接放出connect的實現代碼,寥寥幾行,並不複雜:

export function connect(mapStateToProps, mapDispatchToProps) {    
    return function(Component) {      
        class Connect extends React.Component {        
            componentDidMount() {          
                //從context獲取store並訂閱更新          
                this.context.store.subscribe(this.handleStoreChange.bind(this));        
            }       
            handleStoreChange() {          
                // 觸發更新          
                // 觸發的方法有多種,這裡為了簡潔起見,直接forceUpdate強制更新,讀者也可以通過setState來觸發子組件更新          
                this.forceUpdate()        
            }        
            render() {          
                return (            
                    <Component              
                        // 傳入該組件的props,需要由connect這個高階組件原樣傳回原組件              
                        { ...this.props }              
                        // 根據mapStateToProps把state掛到this.props上              
                        { ...mapStateToProps(this.context.store.getState()) }               
                        // 根據mapDispatchToProps把dispatch(action)掛到this.props上              
                        { ...mapDispatchToProps(this.context.store.dispatch) }                 
                    />              
                )        
            }      
        }      
        //接收context的固定寫法      
        Connect.contextTypes = {        
            store: PropTypes.object      
        }      
        return Connect    
    }
}

寫完了connect的代碼,我們有兩點需要解釋一下:

Provider的意義:我們審視一下connect的代碼,其實context不過是給connect提供了獲取store的途徑,我們在connect中直接import store完全可以取代context。那麼Provider存在的意義是什麼,其實筆者也想過一陣子,後來才想起...上面這個connect是自己寫的,當然可以直接import store,但react-redux的connect是封裝的,對外只提供api,所以需要讓Provider傳入store。

connect中的裝飾器模式:回顧一下connect的調用方式: connect(mapStateToProps, mapDispatchToProps)(App)其實connect完全可以把App跟著mapStateToProps一起傳進去,看似沒必要return一個函數再傳入App,為什麼react-redux要這樣設計,react-redux作為一個被廣泛使用的模塊,其設計肯定有它的深意。

其實connect這種設計,是「裝飾器模式」的實現,所謂裝飾器模式,簡單地說就是對類的一個包裝,動態地拓展類的功能。connect以及React中的高階組件(HoC)都是這一模式的實現。除此之外,也有更直接的原因:這種設計能夠兼容ES7的 &#x88C5;&#x9970;&#x5668;(Decorator),使得我們可以用@connect這樣的方式來簡化代碼,有關@connect的使用可以看這篇:

//普通connect使用
class App extends React.Component{
    render(){
        return <div>hello</div>
    }
}
function mapStateToProps(state){
    return state.main
}
function mapDispatchToProps(dispatch){
    return bindActionCreators(action,dispatch)
}
export default connect(mapStateToProps,mapDispatchToProps)(App)

//使用裝飾器簡化
@connect(
  state=>state.main,
  dispatch=>bindActionCreators(action,dispatch)
)
class App extends React.Component{
    render(){
        return <div>hello</div>
    }
}

寫完了react-redux,我們可以寫個demo來測試一下:使用react-create-app創建一個項目,刪掉無用的文件,並創建store.js、reducer.js、react-redux.js來分別寫我們redux和react-redux的代碼,index.js是項目的入口文件,在App.js中我們簡單的寫一個計數器,點擊按鈕就派發一個dispatch,讓store中的count加一,頁面上顯示這個count。最後文件目錄和代碼如下:

// store.js
export const createStore = (reducer) => {    
    let currentState = {}    
    let observers = []             //觀察者隊列    
    function getState() {        
        return currentState    
    }    
    function dispatch(action) {        
        currentState = reducer(currentState, action)       
        observers.forEach(fn => fn())    
    }    
    function subscribe(fn) {        
        observers.push(fn)    
    }    
    dispatch({ type: '@@REDUX_INIT' }) //初始化store數據    
    return { getState, subscribe, dispatch }
}

//reducer.js
const initialState = {    
    count: 0
}

export function reducer(state = initialState, action) {    
    switch(action.type) {      
        case 'plus':        
        return {            
            ...state,            
            count: state.count + 1        
        }      
        case 'subtract':        
        return {            
            ...state,            
            count: state.count - 1        
        }      
        default:        
        return initialState    
    }
}

//react-redux.js
import React from 'react'
import PropTypes from 'prop-types'
export class Provider extends React.Component {  
    // 需要聲明靜態屬性childContextTypes來指定context對象的屬性,是context的固定寫法  
    static childContextTypes = {    
        store: PropTypes.object  
    }  

    // 實現getChildContext方法,返回context對象,也是固定寫法  
    getChildContext() {    
        return { store: this.store }  
    }  

    constructor(props, context) {    
        super(props, context)    
        this.store = props.store  
    }  

    // 渲染被Provider包裹的組件  
    render() {    
        return this.props.children  
    }
}

export function connect(mapStateToProps, mapDispatchToProps) {    
    return function(Component) {      
    class Connect extends React.Component {        
        componentDidMount() {          //從context獲取store並訂閱更新          
            this.context.store.subscribe(this.handleStoreChange.bind(this));        
        }        
        handleStoreChange() {          
            // 觸發更新          
            // 觸發的方法有多種,這裡為了簡潔起見,直接forceUpdate強制更新,讀者也可以通過setState來觸發子組件更新          
            this.forceUpdate()        
        }        
        render() {          
            return (            
                <Component              
                    // 傳入該組件的props,需要由connect這個高階組件原樣傳回原組件              
                    { ...this.props }              
                    // 根據mapStateToProps把state掛到this.props上              
                    { ...mapStateToProps(this.context.store.getState()) }               
                    // 根據mapDispatchToProps把dispatch(action)掛到this.props上              
                    { ...mapDispatchToProps(this.context.store.dispatch) }             
                />          
            )        
        }      
    }      

    //接收context的固定寫法      
    Connect.contextTypes = {        
        store: PropTypes.object      
    }      
    return Connect    
    }
}  

//index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { Provider } from './react-redux'
import { createStore } from './store'
import { reducer } from './reducer'

ReactDOM.render(   
    <Provider store={createStore(reducer)}>        
        <App />    
    </Provider>,     
    document.getElementById('root')
);

//App.js
import React from 'react'
import { connect } from './react-redux'

const addCountAction = {  
    type: 'plus'
}

const mapStateToProps = state => {  
    return {      
        count: state.count  
    }
}

const mapDispatchToProps = dispatch => {  
    return {      
        addCount: () => {          
            dispatch(addCountAction)      
        }  
    }
}

class App extends React.Component {  
    render() {    
        return (      
            <div className="App">        
                { this.props.count }        
                <button onClick={ () => this.props.addCount() }>增加</button>      
            </div>    
        );  
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

運行項目,點擊增加按鈕,能夠正確的計數,OK大成功,我們整個redux、react-redux的流程就走通了

三. redux Middleware實現

上面redux和react-redux的實現都比較簡單,下面我們來分析實現稍困難一些的「redux中間件」。所謂中間件,我們可以理解為攔截器,用於對某些過程進行攔截和處理,且中間件之間能夠串聯使用。在redux中,我們中間件攔截的是dispatch提交到reducer這個過程,從而增強dispatch的功能。

我查閱了很多redux中間件相關的資料,但最後發現沒有一篇寫的比官方文檔清晰,文檔從中間件的需求到設計,從概念到實現,每一步都有清晰生動的講解。下面我們就和文檔一樣,以一個記錄日誌的中間件為例,一步一步分析redux中間件的設計實現。

我們思考一下,如果我們想在每次dispatch之後,列印一下store的內容,我們會如何實現呢:

1. 在每次dispatch之後手動列印store的內容
store.dispatch({ type: 'plus' })
console.log('next state', store.getState())

這是最直接的方法,當然我們不可能在項目裡每個dispatch後面都粘貼一段列印日誌的代碼,我們至少要把這部分功能提取出來。

2. 封裝dispatch
function dispatchAndLog(store, action) {    
    store.dispatch(action)    
    console.log('next state', store.getState())
}

我們可以重新封裝一個公用的新的dispatch方法,這樣可以減少一部分重複的代碼。不過每次使用這個新的dispatch都得從外部引一下,還是比較麻煩。

3. 替換dispatch
let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {  
    let result = next(action)  
    console.log('next state', store.getState())  
    return result
}

如果我們直接把dispatch給替換,這樣每次使用的時候不就不需要再從外部引用一次了嗎?對於單純列印日誌來說,這樣就足夠了,但是如果我們還有一個監控dispatch錯誤的需求呢,我們固然可以在列印日誌的代碼後面加上捕獲錯誤的代碼,但隨著功能模塊的增多,代碼量會迅速膨脹,以後這個中間件就沒法維護了,我們希望不同的功能是「獨立的可拔插的」模塊。

4. 模塊化
// 列印日誌中間件
function patchStoreToAddLogging(store) {    
    let next = store.dispatch    //此處也可以寫成匿名函數    
    store.dispatch = function dispatchAndLog(action) {      
        let result = next(action)      
        console.log('next state', store.getState())      
        return result    
    }
}  

// 監控錯誤中間件
function patchStoreToAddCrashReporting(store) {    
    //這裡取到的dispatch已經是被上一個中間件包裝過的dispatch, 從而實現中間件串聯    
    let next = store.dispatch    
    store.dispatch = function dispatchAndReportErrors(action) {        
        try {            
            return next(action)        
        } catch (err) {            
            console.error('捕獲一個異常!', err)            
            throw err        
        }    
    }
}

我們把不同功能的模塊拆分成不同的方法,通過在方法內「獲取上一個中間件包裝過的store.dispatch實現鏈式調用」。然後我們就能通過調用這些中間件方法,分別使用、組合這些中間件。

patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)

到這裡我們基本實現了可組合、拔插的中間件,但我們仍然可以把代碼再寫好看一點。我們注意到,我們當前寫的中間件方法都是先獲取dispatch,然後在方法內替換dispatch,這部分重複代碼我們可以再稍微簡化一下:我們不在方法內替換dispatch,而是返回一個新的dispatch,然後讓循環來進行每一步的替換。

5. applyMiddleware

改造一下中間件,使其返回新的dispatch而不是替換原dispatch

function logger(store) {    
    let next = store.dispatch     
 
    // 我們之前的做法(在方法內直接替換dispatch):    
    // store.dispatch = function dispatchAndLog(action) {    
    //         ...    
    // }    
  
    return function dispatchAndLog(action) {        
        let result = next(action)        
        console.log('next state', store.getState())        
        return result    
    }
}

在Redux中增加一個輔助方法applyMiddleware ,用於添加中間件

function applyMiddleware(store, middlewares) {    
    middlewares = [ ...middlewares ]    //淺拷貝數組, 避免下面reserve()影響原數組    
    middlewares.reverse()               //由於循環替換dispatch時,前面的中間件在最裡層,因此需要翻轉數組才能保證中間件的調用順序      
    // 循環替換dispatch   
    middlewares.forEach(middleware =>      
        store.dispatch = middleware(store)    
    )
}

然後我們就能以這種形式增加中間件了:

applyMiddleware(store, [ logger, crashReporter ])

寫到這裡,我們可以簡單地測試一下中間件。我創建了三個中間件,分別是logger1、thunk、logger2,其作用也很簡單,列印logger1 -> 執行異步dispatch -> 列印logger2,我們通過這個例子觀察中間件的執行順序

//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from './react-redux'
import { createStore } from './store'
import { reducer } from './reducer'

let store = createStore(reducer)

function logger(store) {    
    let next = store.dispatch    
    return (action) => {        
        console.log('logger1')        
        let result = next(action)        
        return result    
    }
}

function thunk(store) {    
    let next = store.dispatch    
    return (action) => {        
        console.log('thunk')        
        return typeof action === 'function' ? action(store.dispatch) : next(action)    
    }
}

function logger2(store) {    
    let next = store.dispatch        
    return (action) => {        
        console.log('logger2')        
        let result = next(action)        
        return result    
    }
}

function applyMiddleware(store, middlewares) {    
    middlewares = [ ...middlewares ]      
    middlewares.reverse()     
    middlewares.forEach(middleware =>      
        store.dispatch = middleware(store)    
    )
}

applyMiddleware(store, [ logger, thunk, logger2 ])

ReactDOM.render(    
    <Provider store={store}>        
        <App />    
    </Provider>,     
    document.getElementById('root')
);

發出異步dispatch

function addCountAction(dispatch) {  
    setTimeout(() => {    
        dispatch({ type: 'plus' })  
    }, 1000)
}

dispatch(addCountAction)

輸出結果

可以看到,控制臺先輸出了中間件logger1的列印結果,然後進入thunk中間件列印了'thunk',等待一秒後,異步dispatch被觸發,又重新走了一遍logger1 -> thunk -> logger2。到這裡,我們就基本實現了可拔插、可組合的中間件機制,還順便實現了redux-thunk。

6. 純函數

之前的例子已經基本實現我們的需求,但我們還可以進一步改進,上面這個函數看起來仍然不夠"純",函數在函數體內修改了store自身的dispatch,產生了所謂的"副作用",從函數式編程的規範出發,我們可以進行一些改造,借鑑react-redux的實現思路,我們可以把applyMiddleware作為高階函數,用於增強store,而不是替換dispatch:

先對createStore進行一個小改造,傳入heightener(即applyMiddleware),heightener接收並強化createStore。

// store.js
export const createStore = (reducer, heightener) => {    
    // heightener是一個高階函數,用於增強createStore    
    //如果存在heightener,則執行增強後的createStore    
    if (heightener) {        
        return heightener(createStore)(reducer)    
    }        
    let currentState = {}    
    let observers = []             //觀察者隊列    
    function getState() {        
        return currentState    
    }    
    function dispatch(action) {        
        currentState = reducer(currentState, action);        
        observers.forEach(fn => fn())    
    }    
    function subscribe(fn) {        
        observers.push(fn)    
    }    
    dispatch({ type: '@@REDUX_INIT' })//初始化store數據    
    return { getState, subscribe, dispatch }
}

中間件進一步柯裡化,讓next通過參數傳入

const logger = store => next => action => {    
    console.log('log1')    
    let result = next(action)    
    return result
}

const thunk = store => next =>action => {
    console.log('thunk')    
    const { dispatch, getState } = store    
    return typeof action === 'function' ? action(store.dispatch) : next(action)
}

const logger2 = store => next => action => {    
    console.log('log2')    
    let result = next(action)    
    return result
}

改造applyMiddleware

const applyMiddleware = (...middlewares) => createStore => reducer => {    
    const store = createStore(reducer)    
    let { getState, dispatch } = store    
    const params = {      
        getState,      
        dispatch: (action) => dispatch(action)      
        //解釋一下這裡為什麼不直接 dispatch: dispatch      
        //因為直接使用dispatch會產生閉包,導致所有中間件都共享同一個dispatch,如果有中間件修改了dispatch或者進行異步dispatch就可能出錯    
    }    

    const middlewareArr = middlewares.map(middleware => middleware(params)) 
   
    dispatch = compose(...middlewareArr)(dispatch)    
    return { ...store, dispatch }
}

//compose這一步對應了middlewares.reverse(),是函數式編程一種常見的組合方法
function compose(...fns) {
    if (fns.length === 0) return arg => arg    
    if (fns.length === 1) return fns[0]    
    return fns.reduce((res, cur) =>(...args) => res(cur(...args)))
}

代碼應該不難看懂,在上一個例子的基礎上,我們主要做了兩個改造

使用compose方法取代了middlewares.reverse(),compose是函數式編程中常用的一種組合函數的方式,compose內部使用reduce巧妙地組合了中間件函數,使傳入的中間件函數變成 (...arg) => mid3(mid1(mid2(...arg)))這種形式

不直接替換dispatch,而是作為高階函數增強createStore,最後return的是一個新的store

7.洋蔥圈模型

之所以把洋蔥圈模型放到後面來講,是因為洋蔥圈和前邊中間件的實現並沒有很緊密的關係,為了避免讀者混淆,放到這裡提一下。我們直接放出三個列印日誌的中間件,觀察輸出結果,就能很輕易地看懂洋蔥圈模型。

const logger1 = store => next => action => {    
    console.log('進入log1')    
    let result = next(action)    
    console.log('離開log1')    
    return result
}

const logger2 = store => next => action => {    
    console.log('進入log2')    
    let result = next(action)    
    console.log('離開log2')    
    return result
}

const logger3 = store => next => action => {    
    console.log('進入log3')    
    let result = next(action)    
    console.log('離開log3')    
    return result
}

執行結果

由於我們的中間件是這樣的結構:

logger1(    
    console.log('進入logger1')    
        logger2(        
            console.log('進入logger2')        
                logger3(            
                    console.log('進入logger3')            
                    //dispatch()            
                    console.log('離開logger3')        
                )        
            console.log('離開logger2')    
        )    
    console.log('離開logger1')
)

因此我們可以看到,中間件的執行順序實際上是這樣的:

進入log1 -> 執行next -> 進入log2 -> 執行next -> 進入log3 -> 執行next -> next執行完畢 -> 離開log3 -> 回到上一層中間件,執行上層中間件next之後的語句 -> 離開log2 -> 回到中間件log1, 執行log1的next之後的語句 -> 離開log1

這就是所謂的"洋蔥圈模型"

四. 總結 & 致謝

其實全文看下來,讀者應該能夠體會到,redux、react-redux以及redux中間件的實現並不複雜,各自的核心代碼不過十餘行,但在這寥寥數行代碼之間,蘊含了一系列編程思想與設計範式 —— 觀察者模式、裝飾器模式、中間件原理、函數柯裡化、函數式編程。我們閱讀源碼的意義,也就在於理解和體會這些思想。

全篇成文前後經歷一個月,主要參考資料來自同事分享以及多篇相關文章,在此特別感謝龍超大佬和於中大佬的分享。在考據細節的過程中,也得到了很多素未謀面的朋友們的解惑,特別是感謝Frank1e大佬在中間件柯裡化理解上給予的幫助。真是感謝大家Thanks♪(・ω·)ノ

學習交流添加微信號:is_Nealyang(備註來源) ,入群交流公眾號【全棧前端精選】個人微信【is_Nealyang】

相關焦點

  • React-redux數據傳遞是如何實現的?
    主要內容React數據傳遞reduxReact-redux其他學習目標第一節 react數據傳遞react 中組件之間數據傳遞1. 父傳子2. 子傳父(狀態提升)3.redux能統一管理數據,只要redux中的數據發生改變了,所有使用redux中數據的地方都會改變。redux有自己的一套操作標準。2. 使用1. 安裝:2. 三大原則1.單一數據源整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中。
  • 一步一步教你把 Redux Saga 添加到 React&Redux 程序中
    git clone --branch redux-saga https://github.com/rajjeet/react-quick-start redux-saga-quick-startcd redux-saga-quick-startnpm installnpm start第1步:安裝 redux-saganpm install redux-saga
  • React + Redux + React-router 全家桶
    綜上,redux工作原理如下:Redux中間件Action 發出以後,Reducer 立即算出 State,是同步;過一段時間再執行 Reducer,是異步。中間件能使 Reducer 在異步操作結束後自動執行。中間件本質是一個函數,是在action發出和reducer計算之前觸發的功能回調。
  • 學習 redux 源碼整體架構,深入理解 redux 及其中間件原理
    所以返回的是next函數,他們串起來執行了,形成了中間件的洋蔥模型。人們都說一圖勝千言。我畫了一個相對簡單的redux中間件原理圖。而redux跟react沒有關係,所以它可以使用於小程序或者jQuery等。如果需要和react使用,還需要結合react-redux庫。
  • 完全理解 redux(從零實現一個 redux)
    前言記得開始接觸 react 技術棧的時候,最難理解的地方就是 redux。全是新名詞:reducer、store、dispatch、middleware 等等,我就理解 state 一個名詞。網上找的 redux 文章,要不有一本書的厚度,要不很玄乎,晦澀難懂,越看越覺得難,越看越怕,信心都沒有了!
  • 深度剖析github上15.1k Star項目:redux-thunk
    接下來筆者將從:Redux的工作機制中間件實現原理redux-thunk源碼實現這三個方面來帶大家徹底掌握redux-thunk源碼,從而對redux有更深入的了解和應用。如果大家對react-redux-redux-thunk實戰感興趣的,讀完之後可以移步筆者的《徹底掌握redux》之開發一個任務管理平臺正文在解讀Redux-thunk源碼之前我們需要先掌握redux的基本工作機制和中間件實現原理,這樣才能更好的理解源碼背後的奧義。
  • 「HearLing」React學習之路-redux、react-redux
    四、react-redux可以看到上面我們並沒有使用到react-redux,雖然能實現功能,但細心會發現我是直接拿的store,組件多的話個個拿store,這樣不好。Provider這個還是很好理解的,就是把store直接集成到React應用的頂層props裡面,好處是,所有組件都可以在react-redux的控制之下,所有組件都能訪問到Redux中的數據。
  • 【重學React】動手實現一個react-redux
    react-redux 是什麼react-redux 是 redux 官方 React 綁定庫。它幫助我們連接UI層和數據層。本文目的不是介紹 react-redux 的使用,而是要動手實現一個簡易的 react-redux,希望能夠對你有所幫助。
  • Vuex、Flux、Redux、Redux-saga、Dva、MobX
    顯然,用 Redux 處理異步,可以自己寫中間件來處理,當然大多數人會選擇一些現成的支持異步處理的中間件。比如 redux-thunk 或 redux-promise 。為了簡單處理 Redux 和 React UI 的綁定,一般通過一個叫 react-redux 的庫和 React 配合使用,這個是 react 官方出的(如果不用 react-redux,那麼手動處理 Redux 和 UI 的綁定,需要寫很多重複的代碼,很容易出錯,而且有很多 UI 渲染邏輯的優化不一定能處理好)。
  • React-Redux實戰之——Redux
    當我改變b的name的時候,a也一起變化了,在redux裡我們要保證裡面的每一個data都是immutable的,這樣才能很好的控制數據。2.使用react-reduxRedux其實可以在任何框架中使用,只是因為它和react配合的非常好,所以我們才會一併使用,當你安裝完react-redux後,需要修改代碼如下:
  • 3-6-react-redux
    部分的代碼呢,幾乎不需要怎麼改動,我們還是使用一樣的reducer創建一樣的store.react是專門用來構建用戶界面的框架,那麼自然而然,我們在這裡需要改動的,是我們之前直接用原生js實現的render方法。
  • React-Redux 使用 Hooks
    快速搭建 redux 環境    create-react-app redux-app    yarn  add react-redux reduxTips: 確保 react-redux 版本 在 v7.1.0 以上
  • Redux數據狀態管理
    其中涉及到異步的操作會使用到redux-thunk或redux-saga中間件(中間件指的是store和action的中間,對dispatch方法做了封裝升級,可以dispatch函數)。3.';import { Provider } from 'react-redux';import { PersistGate } from 'redux-persist/integration/react';// renderRoutes 讀取路由配置轉化為 Route 標籤// renderRoutes 這個方法只渲染一層路由import { renderRoutes
  • 關於react結合redux使用,或許你還應該掌握這些
    最近小編委託組內小哥又給自己梳理了一遍react結合redux使用的知識點(因為懶,翻文檔不如白嫖來的開心呀),主要涉及使用的注意事項和使用流程,涉及的中間件以及如何處理異步數據等。完後,小編覺得有必要對這次的知識點做一個系統的整理,造(wu)福(ren)大(zi)眾(di)。
  • 一文梭穿Vuex、Flux、Redux、Redux-saga、Dva、MobX
    顯然,用 Redux 處理異步,可以自己寫中間件來處理,當然大多數人會選擇一些現成的支持異步處理的中間件。比如 redux-thunk 或 redux-promise 。為了簡單處理 Redux 和 React UI 的綁定,一般通過一個叫 react-redux 的庫和 React 配合使用,這個是 react 官方出的(如果不用 react-redux,那麼手動處理 Redux 和 UI 的綁定,需要寫很多重複的代碼,很容易出錯,而且有很多 UI 渲染邏輯的優化不一定能處理好)。
  • 說說面試官:說說對Redux中間件的理解?常用的中間件有哪些?實現原理?
    ,對store.dispatch方法進行了改造,在發出 Action和執行 Reducer這兩步之間,添加了其他功能二、常用的中間件有很多優秀的redux中間件,這裡我們例舉兩個:上述的中間件都需要通過applyMiddlewares進行註冊,作用是將所有的中間件組成一個數組,依次執行
  • redux原理,看這篇就夠了
    建議電腦查看,內容偏多redux創建Store創建redux的store對象,需要調用combineReducers和createStore函數,下面解釋不包含中間件。將這個新函數作為參數傳入createStore函數,函數內部通過dispatch,初始化運行傳入的combination,state生成,返回store對象redux中間件最好把上面看懂之後,再看中間件部分!!下面對中間件進行分析:redux-thunk只是redux中間件的一種,也是比較常見的中間件。red
  • redux原理解析,看這篇就夠了
    建議電腦查看,內容偏多redux創建Store創建redux的store對象,需要調用combineReducers和createStore函數,下面解釋不包含中間件。將這個新函數作為參數傳入createStore函數,函數內部通過dispatch,初始化運行傳入的combination,state生成,返回store對象redux中間件最好把上面看懂之後,再看中間件部分!!下面對中間件進行分析:redux-thunk只是redux中間件的一種,也是比較常見的中間件。red
  • 你還在redux中寫重複囉嗦的樣板代碼嗎?rc-redux-model來了
    { switch (action.type) { case FETCH_USER_INFO_SUCCESS: return Immutable.set(state, 'userInfo', action.data) }}沒錯, 這種樣板代碼,簡直就是 CV 操作,只需要 copy 一份,修改一下名稱,對我個人而言,這會讓我不夠專注,分散管理 const、action
  • 從零開始手寫 redux
    本文旨在理解和實現一個 Redux,但是不會涉及 react-redux(一次深入理解一個知識點即可,react-redux 將出現在下一篇文章中)。2. 從零開始實現一個 `Redux`我們先忘記 Redux 的概念,從一個例子入手,使用 create-react-app 創建一個項目: toredux。代碼地址: myredux/to-redux 中。