閱讀對象:使用過redux,對redux實現原理不是很理解的開發者。javascript
在我實習入職培訓的時候,給我培訓的老哥就跟我說過,redux的核心源碼很簡潔,建議我有空去看一下,提高對redux系列的理解。php
入職一個多月了,已經參與了公司的很多項目,redux也使用了一段時間,對於redux的理解卻一直沒有深刻,還停留在「知道怎麼用,可是不知道其核心原理」的階段。java
因此就在github上拉了redux的源碼,看了一會,發現東西確實很少,比較簡潔。react
在項目中,咱們每每不會純粹的使用redux,而是會配合其餘的一些工具庫提高效率,好比
react-redux
,讓react應用使用redux更容易,相似的也有wepy-redux
,提供給小程序框架wepy的工具庫。git
可是在本文中,咱們討論的範圍就純粹些,僅僅討論redux自己。github
redux自己有哪些做用?咱們先來快速的過一下redux的核心思想(工做流程):編程
在這個工做流程中,redux須要提供的功能是:redux
createStore()
subscribe
,dispatch
,getState
這些方法。combineReducers()
applyMiddleware()
沒錯,就這麼多功能,咱們看下redux的源碼目錄:小程序
確實也就這麼多,至於compose
,bindActionCreators
,則是一些工具方法。數組
下面咱們就逐個來看看createStore
、combineReducers
、applyMiddleware
、compose
的源碼實現。
建議打開連接:redux源碼地址,參照本文的解釋閱讀源碼。
這個函數的大體結構是這樣:
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呢,被存在了閉包中。(不理解閉包的同窗能夠先去了解一下先)
咱們再來詳細的看看每一個模塊是如何實現的(爲了讓邏輯更清晰,省略了錯誤處理的代碼):
function getState() {
return currentState
}
複製代碼
簡單到髮指。其實這很像面向對象編程中封裝只讀屬性的方法,只提供數據的getter方法,而不直接提供setter。(雖然這裏返回的是一個state的引用,你能夠直接修改state,可是通常來講,redux不建議這樣作。)
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都會調用這個沒用的監聽器。
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以前,咱們先來想一想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
這些方法影響輸出)reducer爲何要求使用純函數,文檔裏也有提到,總結下來有這幾點:
state是根據reducer建立出來的,因此reducer是和state緊密相關的,對於state,咱們有時候須要有一些需求(好比打印每一次更新先後的state,或者回到某一次更新前的state)這就對reducer有一些要求。
純函數更易於調試
若是不使用純函數,那麼在比較新舊狀態對應的兩個對象時,咱們就不得不深比較了,深比較是很是浪費性能的。相反的,若是對於全部可能被修改的對象(好比reducer被調用了一次,傳入的state就可能被改變),咱們都新建一個對象並賦值,兩個對象有不一樣的地址。那麼淺比較就能夠了。
至此,咱們已經知道了,reducer是一個純函數,那麼若是咱們在應用中確實須要處理一些反作用(好比異步處理,調用API等操做),那麼該怎麼辦呢?這就是中間件解決的問題。下面咱們就來說講redux中的中間件。
中間件在redux中位於什麼位置,咱們能夠經過這兩張圖來看一下。
先來看看不用中間件時的redux工做流程:
而用了中間件以後的工做流程是這樣的:
那麼中間件該如何融合到redux中呢?
在上面的流程中,2-4的步驟是關於中間件的,但凡咱們想要添加一箇中間件,咱們就須要寫一套2-4的邏輯。
若是咱們須要多箇中間件,咱們就須要考慮如何讓他們串聯起來。若是每次串聯都寫一份串聯邏輯的話,就不夠靈活,萬一須要增刪改或調整中間件的順序,都須要修改中間件串聯的邏輯。
因此redux提供了一種解決方案,將中間件的串聯操做進行了封裝,通過封裝後,上面的步驟2-5就能夠成爲一個總體,以下圖:
咱們只須要改造store自帶的dispatch方法。action發生後,先給中間件處理,最後再dispatch一個action交給reducer去改變狀態。
還記得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的工做方式是:
中間件的工做方式是:
getState
和dispatch
,輸出爲改造函數(改造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的做用:
在applyMiddleware方法中,咱們傳入的「參數」是原始的dispatch方法,返回的「結果」是改造後的dispatch方法。經過compose,咱們可讓多個改造函數抽象成一個改造函數。
做者注:原本只想講redux,可是講着講着卻發現:理解中間件,是理解redux的中間件機制的前提。
下面咱們以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中間件呢?
首先中間件確定是改造dispatch方法,改造後的dispatch應該具備這樣的功能:
如今咱們來看看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
源碼解讀》的博客探究探究這個問題。
敬請期待。
compose()
的源碼解釋unSubscribe
的解釋redux-thunk
爲例