其實筆者本來沒有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的 装饰器(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. 封裝dispatchfunction dispatchAndLog(store, action) {
store.dispatch(action)
console.log('next state', store.getState())
}我們可以重新封裝一個公用的新的dispatch方法,這樣可以減少一部分重複的代碼。不過每次使用這個新的dispatch都得從外部引一下,還是比較麻煩。
3. 替換dispatchlet 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】