首頁>科技>

寫完了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.jsexport 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.jsconst 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.jsimport 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.jsimport 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.jsimport 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.dispatchstore.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.jsimport 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.jsexport 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的是一個新的store7.洋蔥圈模型

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

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♪(・ω·)ノ

最新評論
  • 整治雙十一購物亂象,國家再次出手!該跟這些套路說再見了
  • 莫欺少年窮——萬網創始人張向東