redux真的不復雜——源碼解讀

前言

閱讀對象:使用過redux,對redux實現原理不是很理解的開發者。javascript

在我實習入職培訓的時候,給我培訓的老哥就跟我說過,redux的核心源碼很簡潔,建議我有空去看一下,提高對redux系列的理解。php

入職一個多月了,已經參與了公司的很多項目,redux也使用了一段時間,對於redux的理解卻一直沒有深刻,還停留在「知道怎麼用,可是不知道其核心原理」的階段。java

因此就在github上拉了redux的源碼,看了一會,發現東西確實很少,比較簡潔。react

redux自己的功能是什麼

在項目中,咱們每每不會純粹的使用redux,而是會配合其餘的一些工具庫提高效率,好比react-redux,讓react應用使用redux更容易,相似的也有wepy-redux,提供給小程序框架wepy的工具庫。git

可是在本文中,咱們討論的範圍就純粹些,僅僅討論redux自己github

redux自己有哪些做用?咱們先來快速的過一下redux的核心思想(工做流程):編程

  • 將狀態統一放在一個state中,由store來管理這個state。
  • 這個store按照reducer的「shape」(形狀)建立。
  • reducer的做用是接收到action後,輸出一個新的狀態,對應地更新store上的狀態。
  • 根據redux的原則指導,外部改變state的最佳方式是經過調用store的dispatch方法,觸發一個action,這個action被對應的reducer處理,完成state更新。
  • 能夠經過subscribe在store上添加一個監聽函數。每當調用dispatch方法時,會執行全部的監聽函數。
  • 能夠添加中間件(中間件是幹什麼的咱們後面講)處理反作用。

在這個工做流程中,redux須要提供的功能是:redux

  • 建立store,即:createStore()
  • 建立出來的store提供subscribedispatchgetState這些方法。
  • 將多個reducer合併爲一個reducer,即:combineReducers()
  • 應用中間件,即applyMiddleware()

沒錯,就這麼多功能,咱們看下redux的源碼目錄:小程序

redux的源碼目錄

確實也就這麼多,至於compose,bindActionCreators,則是一些工具方法。數組

下面咱們就逐個來看看createStorecombineReducersapplyMiddlewarecompose的源碼實現。

建議打開連接:redux源碼地址,參照本文的解釋閱讀源碼。

createStore的實現

這個函數的大體結構是這樣:

function createStore(reducer, preloadedState, enhancer) {
    if(enhancer是有效的){  // 這個咱們後面會解釋,能夠先忽略
        return enhancer(createStore)(reducer, preloadedState)
    } 
    
    let currentReducer = reducer // 當前store中的reducer
    let currentState = preloadedState // 當前store中存儲的狀態
    let currentListeners = [] // 當前store中放置的監聽函數
    let nextListeners = currentListeners // 下一次dispatch時的監聽函數
    // 注意:當咱們新添加一個監聽函數時,只會在下一次dispatch的時候生效。
    
    //...
    
    // 獲取state
    function getState() {
        //...
    }
    
    // 添加一個監聽函數,每當dispatch被調用的時候都會執行這個監聽函數
    function subscribe() {
        //...
    }
    
    // 觸發了一個action,所以咱們調用reducer,獲得的新的state,而且執行全部添加到store中的監聽函數。
    function dispatch() {
        //...
    }
   
    //...
    
    //dispatch一個用於初始化的action,至關於調用一次reducer
    //而後將reducer中的子reducer的初始值也獲取到
    //詳見下面reducer的實現。
    
    
    return {
        dispatch,
        subscribe,
        getState,
        //下面兩個是主要面向庫開發者的方法,暫時先忽略
        //replaceReducer,
        //observable
    }
}
複製代碼

能夠看出,createStore方法建立了一個store,可是並無直接將這個store的狀態state返回,而是返回了一系列方法,外部能夠經過這些方法(getState)獲取state,或者間接地(經過調用dispatch)改變state。

至於state呢,被存在了閉包中。(不理解閉包的同窗能夠先去了解一下先)

咱們再來詳細的看看每一個模塊是如何實現的(爲了讓邏輯更清晰,省略了錯誤處理的代碼):

getState
function getState() {
    return currentState
}
複製代碼

簡單到髮指。其實這很像面向對象編程中封裝只讀屬性的方法,只提供數據的getter方法,而不直接提供setter。(雖然這裏返回的是一個state的引用,你能夠直接修改state,可是通常來講,redux不建議這樣作。)

subscribe
function subscribe(listener) {
    // 添加到監聽函數數組,
    // 注意:咱們添加到了下一次dispatch時纔會生效的數組
    nextListeners.push(listener)
    
    let isSubscribe = true //設置一個標誌,標誌該監聽器已經訂閱了
    // 返回取消訂閱的函數,即從數組中刪除該監聽函數
    return function unsubscribe() {
        if(!isSubscribe) {
            return // 若是已經取消訂閱過了,直接返回
        }
        
        isSubscribe = false
        // 從下一輪的監聽函數數組(用於下一次dispatch)中刪除這個監聽器。
        const index = nextListeners.indexOf(listener)
        nextListeners.splice(index, 1)
    }
}
複製代碼

subscribe返回的是一個取消訂閱的方法。取消訂閱是很是必要的,當添加的監聽器沒用了以後,應該從store中清理掉。否則每次dispatch都會調用這個沒用的監聽器。

dispatch
function dispatch(action) {
    //調用reducer,獲得新state
    currentState = currentReducer(currentState, action);
    
    //更新監聽數組
    currentListener = nextListener;
    //調用監聽數組中的全部監聽函數
    for(let i = 0; i < currentListener.length; i++) {
        const listener = currentListener[i];
        listener();
    }
}
複製代碼

createStore這個方法的基本功能咱們已經實現了,可是調用createStore方法須要提供reducer,讓咱們來思考一下reducer的做用。

combineReducers

在理解combineReducers以前,咱們先來想一想reducer的功能:reducer接受一箇舊的狀態和一個action,當這個action被觸發的時候,reducer處理後返回一個新狀態。

也就是說 ,reducer負責狀態的管理(或者說更新)。在實際使用中,咱們應用的狀態是能夠分紅不少個模塊的,好比一個典型社交網站的狀態能夠分爲:用戶我的信息,好友列表,消息列表等模塊。理論上,咱們能夠用一個reducer去處理全部狀態的維護,可是這樣作的話,咱們一個reducer函數的邏輯就會太多,容易產生混亂。

所以咱們能夠將邏輯(reducer)也按照模塊劃分,每一個模塊再細分紅各個子模塊,開發完每一個模塊的邏輯後,再將reducer合併起來,這樣咱們的邏輯就能很清晰的組合起來。

對於咱們的這種需求,redux提供了combineReducers方法,能夠把子reducer合併成一個總的reducer。

來看看redux源碼中combineReducers的主要邏輯:

function combineReducers(reducers) {
    //先獲取傳入reducers對象的全部key
    const reducerKeys = Object.keys(reducers)
    const finalReducers = {} // 最後真正有效的reducer存在這裏
    
    //下面從reducers中篩選出有效的reducer
    for(let i = 0; i < reducerKeys.length; i++){
        const key  = reducerKeys[i]
        
        if(typeof reducers[key] === 'function') {
            finalReducers[key] = reducers[key] 
        }
    }
    const finalReducerKeys = Object.keys(finalReducers);
    
    //這裏assertReducerShape函數作的事情是:
    // 檢查finalReducer中的reducer接受一個初始action或一個未知的action時,是否依舊可以返回有效的值。
    let shapeAssertionError
  	try {
    	assertReducerShape(finalReducers)
  	} catch (e) {
    	shapeAssertionError = e
  	}
    
    //返回合併後的reducer
    return function combination(state= {}, action){
  		//這裏的邏輯是:
    	//取得每一個子reducer對應的state,與action一塊兒做爲參數給每一個子reducer執行。
    	let hasChanged = false //標誌state是否有變化
        let nextState = {}
        for(let i = 0; i < finalReducerKeys.length; i++) {
                    //獲得本次循環的子reducer
            const key = finalReducerKeys[i]
            const reducer = finalReducers[key]
            //獲得該子reducer對應的舊狀態
            const previousStateForKey = state[key]
            //調用子reducer獲得新狀態
            const nextStateForKey = reducer(previousStateForKey, action)
            //存到nextState中(總的狀態)
            nextState[key] = nextStateForKey
            //到這裏時有一個問題:
            //就是若是子reducer不能處理該action,那麼會返回previousStateForKey
            //也就是舊狀態,當全部狀態都沒改變時,咱們直接返回以前的state就能夠了。
            hasChanged = hasChanged || previousStateForKey !== nextStateForKey
        }
        return hasChanged ? nextState : state
    }
} 
複製代碼

爲何須要中間件

在redux的設計思想中,reducer應該是一個純函數

維基百科關於純函數的定義:

程序設計中,若一個函數符合如下要求,則它可能被認爲是純函數

  • 此函數在相同的輸入值時,需產生相同的輸出。函數的輸出和輸入值之外的其餘隱藏信息或狀態無關,也和由I/O設備產生的外部輸出無關。
  • 該函數不能有語義上可觀察的函數反作用,諸如「觸發事件」,使輸出設備輸出,或更改輸出值之外物件的內容等。

純函數的輸出能夠不用和全部的輸入值有關,甚至能夠和全部的輸入值都無關。但純函數的輸出不能和輸入值之外的任何資訊有關。純函數能夠傳回多個輸出值,但上述的原則需針對全部輸出值都要成立。若引數是傳引用調用,如有對參數物件的更改,就會影響函數之外物件的內容,所以就不是純函數。

總結一下,純函數的重點在於:

  • 相同的輸入產生相同的輸出(不能在內部使用Math.random,Date.now這些方法影響輸出)
  • 輸出不能和輸入值之外的任何東西有關(不能調用API得到其餘數據)
  • 函數內部不能影響函數外部的任何東西(不能直接改變傳入的引用變量),即不會突變

reducer爲何要求使用純函數,文檔裏也有提到,總結下來有這幾點:

  • state是根據reducer建立出來的,因此reducer是和state緊密相關的,對於state,咱們有時候須要有一些需求(好比打印每一次更新先後的state,或者回到某一次更新前的state)這就對reducer有一些要求。

  • 純函數更易於調試

    • 好比咱們調試時但願action和對應的新舊state可以被打印出來,若是新state是在舊state上修改的,即便用同一個引用,那麼就不能打印出新舊兩種狀態了。
    • 若是函數的輸出具備隨機性,或者依賴外部的任何東西,都會讓咱們調試時很難定位問題。
  • 若是不使用純函數,那麼在比較新舊狀態對應的兩個對象時,咱們就不得不深比較了,深比較是很是浪費性能的。相反的,若是對於全部可能被修改的對象(好比reducer被調用了一次,傳入的state就可能被改變),咱們都新建一個對象並賦值,兩個對象有不一樣的地址。那麼淺比較就能夠了。

至此,咱們已經知道了,reducer是一個純函數,那麼若是咱們在應用中確實須要處理一些反作用(好比異步處理,調用API等操做),那麼該怎麼辦呢?這就是中間件解決的問題。下面咱們就來說講redux中的中間件。

中間件處理反作用的機制

中間件在redux中位於什麼位置,咱們能夠經過這兩張圖來看一下。

先來看看不用中間件時的redux工做流程:

redux工做流程_同步

  1. dispatch一個action(純對象格式)
  2. 這個action被reducer處理
  3. reducer根據action更新store(中的state)

而用了中間件以後的工做流程是這樣的:

redux工做流程_中間件

  1. dispatch一個「action」(不必定是標準的action)
  2. 這個「action」先被中間件處理(好比在這裏發送一個異步請求)
  3. 中間件處理結束後,再發送一個"action"(有多是原來的action,也多是不一樣的action因中間件功能不一樣而不一樣)
  4. 中間件發出的"action"可能繼續被另外一箇中間件處理,進行相似3的步驟。即中間件能夠鏈式串聯。
  5. 最後一箇中間件處理完後,dispatch一個符合reducer處理標準的action(純對象action)
  6. 這個標準的action被reducer處理,
  7. reducer根據action更新store(中的state)

那麼中間件該如何融合到redux中呢?

在上面的流程中,2-4的步驟是關於中間件的,但凡咱們想要添加一箇中間件,咱們就須要寫一套2-4的邏輯。

若是咱們須要多箇中間件,咱們就須要考慮如何讓他們串聯起來。若是每次串聯都寫一份串聯邏輯的話,就不夠靈活,萬一須要增刪改或調整中間件的順序,都須要修改中間件串聯的邏輯。

因此redux提供了一種解決方案,將中間件的串聯操做進行了封裝,通過封裝後,上面的步驟2-5就能夠成爲一個總體,以下圖:

封裝中間件後的邏輯

咱們只須要改造store自帶的dispatch方法。action發生後,先給中間件處理,最後再dispatch一個action交給reducer去改變狀態。

在redux中使用中間件

還記得redux 的createStore()方法的第三個參數enhancer嗎:

function createStore(reducer, preloadedState, enhancer) {
    if(enhancer是有效的){  
        return enhancer(createStore)(reducer, preloadedState)
    } 
    
    //...
}
複製代碼

在這裏,咱們能夠看到,enhancer(能夠叫作強化器)是一個函數,這個函數接受一個「普通createStore函數」做爲參數,返回一個「增強後的createStore函數」。

這個增強的過程當中作的事情,其實就是改造dispatch,添加上中間件。

redux提供的applyMiddleware()方法返回的就是一個enhancer。

applyMiddleware,顧名思義,「應用中間件」。輸入爲若干中間件,輸出爲enhancer。下面來看看它的源碼:

function applyMiddleware(...middlewares) {
    // 返回一個函數A,函數A的參數是一個createStore函數。
    // 函數A的返回值是函數B,其實也就是一個增強後的createStore函數,大括號內的是函數B的函數體
    return createStore => (...args) => {
        //用參數傳進來的createStore建立一個store
        const store  = createStore(...args)
        //注意,咱們在這裏須要改造的只是store的dispatch方法
        
        let dispatch = () => {  //一個臨時的dispatch
            					//做用是在dispatch改造完成前調用dispatch只會打印錯誤信息
            throw new Error(`一些錯誤信息`)
        } 
        //接下來咱們準備將每一箇中間件與咱們的state關聯起來(經過傳入getState方法),獲得改造函數。
        const middlewareAPI = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args)
        }
        //middlewares是一箇中間件函數數組,中間件函數的返回值是一個改造dispatch的函數
        //調用數組中的每一箇中間件函數,獲得全部的改造函數
        const chain = middlewares.map(middleware => middleware(middlewareAPI))
        
        //將這些改造函數compose(翻譯:構成,整理成)成一個函數
        //用compose後的函數去改造store的dispatch
        dispatch = compose(...chain)(store.dispatch)
        // compose方法的做用是,例如這樣調用:
        // compose(func1,func2,func3)
        // 返回一個函數: (...args) => func1( func2( func3(...args) ) )
        // 即傳入的dispatch被func3改造後獲得一個新的dispatch,新的dispatch繼續被func2改造...
        
        // 返回store,用改造後的dispatch方法替換store中的dispatch
        return {
            ...store,
            dispatch
        }
    }
}
複製代碼

總結一下,applyMiddleware的工做方式是:

  1. 調用(若干個)中間件函數,獲取(若干個)改造函數
  2. 把全部改造函數compose成一個改造函數
  3. 改造dispatch方法

中間件的工做方式是:

  • 中間件是一個函數,不妨叫作中間件函數
  • 中間件函數的輸入是store的getStatedispatch,輸出爲改造函數(改造dispatch的函數)
  • 改造函數輸入是一個dispatch,輸出「改造後的dispatch

源碼中用到了一個頗有用的方法:compose(),將多個函數組合成一個函數。理解這個函數對理解中間件頗有幫助,咱們來看看它的源碼:

function compose(...funcs) {
    // 當未傳入函數時,返回一個函數:arg => arg
    if(funcs.length === 0) {
        return arg => arg
    }
    
    // 當只傳入一個函數時,直接返回這個函數
    if(funcs.length === 1) {
        return funcs[0]
    }
    
    // 返回組合後的函數
    return funcs.reduce((a, b) => (...args) => a(b(...args)))
    
    //reduce是js的Array對象的內置方法
    //array.reduce(callback)的做用是:給array中每個元素應用callback函數
    //callback函數:
    /* *@參數{accumulator}:callback上一次調用的返回值 *@參數{value}:當前數組元素 *@參數{index}:可選,當前元素的索引 *@參數{array}:可選,當前數組 * *callback( accumulator, value, [index], [array]) */
}
複製代碼

畫一張圖來理解compose的做用:

compose

在applyMiddleware方法中,咱們傳入的「參數」是原始的dispatch方法,返回的「結果」是改造後的dispatch方法。經過compose,咱們可讓多個改造函數抽象成一個改造函數。

中間件的實現

做者注:原本只想講redux,可是講着講着卻發現:理解中間件,是理解redux的中間件機制的前提。

下面咱們以redux-thunk爲例,看看一箇中間件是如何實現的。

redux-thunk的功能

你可能沒用過redux-thunk,因此在閱讀源碼前,我先簡要的講一下redux-thunk的做用:

正常的dispatch函數的參數action應該是一個純對象。像這樣:

store.dispatch({
    type:'REQUEST_SOME_THING',
    payload: {
        from:'bob',
    }
})
複製代碼

使用了thunk以後,咱們能夠dispatch一個函數:

function logStateInOneSecond(name) {
    return (dispatch, getState, name) => {  // 這個函數會在合適的時候dispatch一個真正的action
        setTimeout({
            console.log(getState())
            dispatch({
                type:'LOG_OK',
                payload: {
                    name,
                }
            })
        }, 1000)
    }
}

store.dispatch(logStateInOneSecond('jay')) //dispatch的參數是一個函數
複製代碼

爲何須要這個功能?或者說「dispatch一個函數」能解決什麼問題?

從上面的例子中你會發現,若是dispatch一個函數,咱們能夠在這個函數內作任何咱們想要的操做(異步處理,調用接口等等),不受任何限制,爲何?

由於咱們「尚未dispatch一個真正的action」,因此不會調用reducer,咱們並無將反作用放在reducer中,而是在使用reducer以前就處理了反作用

若是你還不明白redux-thunk的功能,能夠去它的github倉庫查看更詳細的解釋。

redux-thunk的實現

如何實現redux-thunk中間件呢?

首先中間件確定是改造dispatch方法,改造後的dispatch應該具備這樣的功能:

  1. 若是傳入的參數是函數,就執行這個函數。
  2. 不然,就認爲傳入的是一個標準的action,就調用「改造前的dispatch」方法,dispatch這個action。

如今咱們來看看redux-thunk的源碼(8行有效代碼):

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}
複製代碼

若是三個箭頭函數讓你有點頭暈,我來幫你展開一下:

//createThunkMiddleware的做用是返回thunk中間件(middleware)
function createThunkMiddleware(extraArgument) {
    
    return function({ dispatch, getState }) { // 這是「中間件函數」
        
        return function(next) { // 這是中間件函數建立的「改造函數」
            
            return function(action) { // 這是改造函數改造後的「dispatch方法」
                if (typeof action === 'function') {
                  return action(dispatch, getState, extraArgument);
                }
                
                return next(action);
            }
        } 
    }
}
複製代碼

再加點註釋?

function createThunkMiddleware(extraArgument) {
    
    return function({ dispatch, getState }) { // 這是「中間件函數」
        //參數是store中的dispatch和getState方法
        
        return function(next) { // 這是中間件函數建立的「改造函數」
            //參數next是被當前中間件改造前的dispatch
            //由於在被當前中間件改造以前,可能已經被其餘中間件改造過了,因此不妨叫next
            
            return function(action) { // 這是改造函數「改造後的dispatch方法」
                if (typeof action === 'function') {
                  //若是action是一個函數,就調用這個函數,並傳入參數給函數使用
                  return action(dispatch, getState, extraArgument);
                }
                
                //不然調用用改造前的dispatch方法
                return next(action);
            }
        } 
    }
}
複製代碼

講完了。能夠看出redux-thunk嚴格遵循了redux中間件的思想:在原始的dispatch方法觸發reducer處理以前,處理反作用。

總結

至此,redux的核心源碼已經講完了,最後不得不感嘆,redux寫的真的美,真tm的簡潔。

一句話總結redux的核心功能:「建立一個store來管理state」

關於中間件,我會嘗試着寫一篇《如何本身實現一個redux中間件》,更深刻的理解redux中間件的意義。

關於store如何與其餘框架(如react)共同工做,我會再寫一篇《react-redux源碼解讀》的博客探究探究這個問題。

敬請期待。


第一次更新(2018-09-12)

  • 添加了對compose()的源碼解釋
  • 修正了錯誤的描述」改變state的惟一方式是觸發dispatch」;補充了關於unSubscribe的解釋
  • 添加了中間件的實現原理,以redux-thunk爲例
相關文章
相關標籤/搜索