其實筆者原本沒有redux相關的行文計劃,不過公司內部最近有同事做了redux相關的技術分享,而筆者承擔了一部分文章評審的任務,在評審的過程當中,筆者花了至關的精力時間來查閱資料和實現代碼,先後積攢了幾千字的筆記,對redux也有了一份心得看法,因而順手寫就本文,但願能給你們帶來些一些啓發和思考Thanks♪(・ω·)ノ通過本文的學習,讀者應該可以學習理解:javascript
redux
的設計思路及實現原理java
react-redux
的設計思路及實現原理react
redux中間件
的設計思路及實現原理編程
在一切開始以前,咱們首先要回答一個問題:爲何咱們須要redux,redux爲咱們解決了什麼問題?只有回答了這個問題,咱們才能把握redux的設計思路。redux
React做爲一個組件化開發框架,組件之間存在大量通訊,有時這些通訊跨越多個組件,或者多個組件之間共享一套數據,簡單的父子組件間傳值不能知足咱們的需求,天然而然地,咱們須要有一個地方存取和操做這些公共狀態。而redux就爲咱們提供了一種管理公共狀態的方案,咱們後續的設計實現也將圍繞這個需求來展開。設計模式
咱們思考一下如何管理公共狀態:既然是公共狀態,那麼就直接把公共狀態提取出來好了。咱們建立一個store.js文件,而後直接在裏邊存放公共的state,其餘組件只要引入這個store就能夠存取共用狀態了。api
const state = {
count: 0
}複製代碼
咱們在store裏存放一個公共狀態count,組件在import了store後就能夠操做這個count。這是最直接的store,固然咱們的store確定不能這麼設計,緣由主要是兩點:數組
1. 容易誤操做安全
好比說,有人一個不當心把store賦值了{},清空了store,或者誤修改了其餘組件的數據,那顯然不太安全,出錯了也很難排查,所以咱們須要有條件地操做store,防止使用者直接修改store的數據。bash
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 }
}複製代碼
getState()的實現很是簡單,返回當前狀態便可:
export const createStore = () => {
let currentState = {} // 公共狀態
function getState() { // getter
return currentState
}
function dispatch() {} // setter
function subscribe() {} // 發佈訂閱
return { getState, dispatch, subscribe }
}複製代碼
可是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 }
儘管咱們已經可以存取公用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()傳入的回調的執行結果:
到這裏,一個簡單的redux就已經完成,在redux真正的源碼中還加入了入參校驗等細節,但整體思路和上面的基本相同。
咱們已經能夠在組件裏引入store進行狀態的存取以及訂閱store變化,數一下,正好十行代碼(`∀´)Ψ。可是咱們看一眼右邊的進度條,就會發現事情並不簡單,篇幅到這裏才過了三分之一。儘管說咱們已經實現了redux,但coder們並不知足於此,咱們在使用store時,須要在每一個組件中引入store,而後getState,而後dispatch,還有subscribe,代碼比較冗餘,咱們須要合併一些重複操做,而其中一種簡化合並的方案,就是咱們熟悉的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:
咱們先從比較簡單的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。
下面咱們來思考一下如何實現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。最後文件目錄和代碼以下:
// 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和react-redux的實現都比較簡單,下面咱們來分析實現稍困難一些的redux中間件。所謂中間件,咱們能夠理解爲攔截器,用於對某些過程進行攔截和處理,且中間件之間可以串聯使用。在redux中,咱們中間件攔截的是dispatch提交到reducer這個過程,從而加強dispatch的功能。
我查閱了不少redux中間件相關的資料,但最後發現沒有一篇寫的比官方文檔清晰,文檔從中間件的需求到設計,從概念到實現,每一步都有清晰生動的講解。下面咱們就和文檔同樣,以一個記錄日誌的中間件爲例,一步一步分析redux中間件的設計實現。
咱們思考一下,若是咱們想在每次dispatch以後,打印一下store的內容,咱們會如何實現呢:
store.dispatch({ type: 'plus' })
console.log('next state', store.getState())複製代碼
這是最直接的方法,固然咱們不可能在項目裏每一個dispatch後面都粘貼一段打印日誌的代碼,咱們至少要把這部分功能提取出來。
function dispatchAndLog(store, action) {
store.dispatch(action)
console.log('next state', store.getState())
}複製代碼
咱們能夠從新封裝一個公用的新的dispatch方法,這樣能夠減小一部分重複的代碼。不過每次使用這個新的dispatch都得從外部引一下,仍是比較麻煩。
let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
let result = next(action)
console.log('next state', store.getState())
return result
}複製代碼
若是咱們直接把dispatch給替換,這樣每次使用的時候不就不須要再從外部引用一次了嗎?對於單純打印日誌來講,這樣就足夠了,可是若是咱們還有一個監控dispatch錯誤的需求呢,咱們當然能夠在打印日誌的代碼後面加上捕獲錯誤的代碼,但隨着功能模塊的增多,代碼量會迅速膨脹,之後這個中間件就無法維護了,咱們但願不一樣的功能是獨立的可拔插的模塊。
// 打印日誌中間件
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,而後讓循環來進行每一步的替換。
改造一下中間件,使其返回新的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 ])複製代碼
寫到這裏,咱們能夠簡單地測試一下中間件。我建立了三個中間件,分別是logger一、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。
以前的例子已經基本實現咱們的需求,但咱們還能夠進一步改進,上面這個函數看起來仍然不夠「純」,函數在函數體內修改了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
之因此把洋蔥圈模型放到後面來說,是由於洋蔥圈和前邊中間件的實現並無很緊密的關係,爲了不讀者混淆,放到這裏提一下。咱們直接放出三個打印日誌的中間件,觀察輸出結果,就能很輕易地看懂洋蔥圈模型。
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♪(・ω·)ノ