10行代碼看盡redux原理——全面剖析redux|reactredux|redux中間件設計實現(近8k字)

NO IMAGE

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

  1. redux的設計思路及實現原理

  2. react-redux的設計思路及實現原理

  3. 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的含義還得根據上下文推斷,所以我們最好是給每個操作起個名字

項目交接

10行代碼看盡redux原理——全面剖析redux|reactredux|redux中間件設計實現(近8k字)

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

既然我們要存取狀態,那麼肯定要有gettersetter,此外當狀態發生改變時,我們得進行廣播,通知組件狀態發生了變更。這不就和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 }

10行代碼看盡redux原理——全面剖析redux|reactredux|redux中間件設計實現(近8k字)

3.subscribe實現

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

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

//被觀察者
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()傳入的回調的執行結果:

10行代碼看盡redux原理——全面剖析redux|reactredux|redux中間件設計實現(近8k字)

到這裡,一個簡單的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提供Providerconnect兩個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的代碼,我們有兩點需要解釋一下:

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

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

其實connect這種設計,是裝飾器模式的實現,所謂裝飾器模式,簡單地說就是對類的一個包裝,動態地拓展類的功能。connect以及React中的高階組件(HoC)都是這一模式的實現。除此之外,也有更直接的原因:這種設計能夠兼容ES7的裝飾器(Decorator),使得我們可以用@connect這樣的方式來簡化代碼,有關@connect的使用可以看這篇<react-redux中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。最後文件目錄和代碼如下:

10行代碼看盡redux原理——全面剖析redux|reactredux|redux中間件設計實現(近8k字)

// 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的流程就走通了

10行代碼看盡redux原理——全面剖析redux|reactredux|redux中間件設計實現(近8k字)

三. redux Middleware實現

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

10行代碼看盡redux原理——全面剖析redux|reactredux|redux中間件設計實現(近8k字)

    我查閱了很多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)

    輸出結果

    10行代碼看盡redux原理——全面剖析redux|reactredux|redux中間件設計實現(近8k字)

    可以看到,控制檯先輸出了中間件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)))
    }

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

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

    2. 不直接替換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
    }

    執行結果

    10行代碼看盡redux原理——全面剖析redux|reactredux|redux中間件設計實現(近8k字)

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

    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

    這就是所謂的“洋蔥圈模型”

    10行代碼看盡redux原理——全面剖析redux|reactredux|redux中間件設計實現(近8k字)

    四. 總結 & 致謝

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

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

    相關文章

    GraphQL入門到實踐

    java中的”鎖”事

    可迭代對象與類數組對象

    Docker搭建你的第一個Node項目到服務器(完整版)