摘要:到月底了,小明的爸爸的單位發了工資總計塊大洋,拿到工資之后第一件的事情就是上交,毫無疑問的,除非小明爸爸不要命了。當小明的爸爸收到這個通知之后,心的一塊大石頭也就放下來了。下面我們正式開始我們的源碼閱讀之旅。
前言
用過react的小伙伴對redux其實并不陌生,基本大多數的React應用用到它。一般大家用redux的時候基本都不會多帶帶去使用它,而是配合react-redux一起去使用。剛學習redux的時候很容易弄混淆redux和react-redux,以為他倆是同一個東西。其實不然,redux是javascript應用程序的可預測狀態容器,而react-redux則是用來連接這個狀態容器與react組件。可能前端新人對這兩者還是覺得很抽象,打個比方說,在一個普通家庭中,媽媽在家里都是至高無上的地位,掌握家中經濟大權,家里的經濟流水都要經過你的媽媽,而你的爸爸則負責從外面賺錢然后交給你的媽媽。這里把你的媽媽類比成redux,而你的爸爸可以類比成react-redux,而外面的大千世界則是react組件。相信這樣的類比,大家對這react和react-redux的有了一個初步認識。本篇文章介紹的主要內容是對redux的源碼的分析,react-redux的源碼分析將會在我的下一篇文章中,敬請期待!各位小伙們如果覺得寫的不錯的話,麻煩多多點贊收藏關注哦!
redux的使用在講redux的源碼之前,我們先回顧一下redux是如何使用的,然后我們再對照著redux的使用去閱讀源碼,這樣大家的印象可能會更加深刻點。先貼上一段demo代碼:
const initialState={ cash:200, } const reducer=(state=initialState,action)=>{ const {type,payload} = action; switch(type){ case "INCREMENT": return Object.assign({},state,{ cash:state.cash+payload }); case "DECREMENT": return Object.assign({},state,{ cash:state.cash-payload }); default : return state; } } const reducers=Redux.combineReducers({treasury:reducer}); //創建小金庫 const store=Redux.createStore(reducers); //當小金庫的現金發生變化時,打印當前的金額 store.subscribe(()=>{ console.log(`余額:${store.getState().treasury.cash}`); }); //小明爸爸發了工資300塊上交 store.dispatch({ type:"INCREMENT", payload:300 }); //小明拿著水電費單交100塊水電費 store.dispatch({ type:"DECREMENT", payload:100 });
上面這段代碼是一個非常典型的redux的使用,跟大家平時在項目里用的不太一樣,可能有些小伙伴們不能理解,其實react-redux只不過在這種使用方法上做了一層封裝。等當我們弄清楚redux的使用,再去看react-redux源碼便會明白了我們在項目里為何是那種寫法而不是這種寫法。
說到redux的使用,不免要說一下action、reducer和store三者的關系。記得當初第一次使用redux的時候,一直分不清這三者的關系,感覺這三個很抽象很玄學,相信不少小伙伴們跟我一樣遇到過同樣的情況。其實并不難,我還是用文章開頭打的比方還解釋這三者的關系。
現在保險箱(store)里存放200塊大洋。到月底了,小明的爸爸的單位發了工資總計300塊大洋,拿到工資之后第一件的事情就是上交,毫無疑問的,除非小明爸爸不要命了。小明的爸爸可以直接將這300塊大洋放到家里的保險箱里面嗎?顯然是不可以的,所以小明的爸爸得向小明的爸爸提交申請,而這個申請也就是我們所說的action。這個申請(action)包括操作類型和對應的東西,申請類型就是存錢(INCREMENT),對應的東西就是300塊大洋(payload)。此時小明的媽媽拿到這個申請之后,將根據這個申請執行對應的操作,這里就是往保險箱里的現金里放300塊大洋進去,此時小明的媽媽干的事情就是reducer干的事情。當300塊大洋放完之后,小明的媽媽就通知家里的所有人現在的小金庫的金額已經發生了變化,現在的余額是500塊。當小明的爸爸收到這個通知之后,心的一塊大石頭也就放下來了。過了一會,小明回來了,并且拿著一張價值100塊的水電費的催收單。于是,小明想小明媽媽申請交水電費,小明媽媽從保險庫中取出來100塊給了小明,并通知了家里所有人小金庫的金額又發生了變化,現在余額400塊。
通過上面的例子,相信小伙們對三者的關系有了一個比較清晰的認識。現在我們已經理清楚了action、reducer和store三者的關系,并且也知道了redux是如何使用的了,現在將開始我們得源碼閱讀之旅。
redux項目結構本篇文章是基于redux的4.0.0版本做的源碼分析,小伙伴們在對照源碼的時候,千萬別弄錯了。整個redux項目的源碼的閱讀我們只需要關注src的目錄即可。
這里主要分為兩大塊,一塊為自定義的工具庫,另一塊則是redux的邏輯代碼。先從哪塊開始閱讀呢?我個人建議先閱讀自定義的工具庫這塊。主要有這么兩個原因:第一個,這塊代碼比較簡單,容易理解,大家更能進入閱讀的狀態;第二個,redux邏輯代碼會用到這些自定義工具,先搞懂這些,對后續邏輯代碼的閱讀做了一個很好的鋪墊。下面我們正式開始我們的源碼閱讀之旅。
utils actionTypes.jsconst ActionTypes = { INIT: "@@redux/INIT" + Math.random() .toString(36) .substring(7) .split("") .join("."), REPLACE: "@@redux/REPLACE" + Math.random() .toString(36) .substring(7) .split("") .join(".") } export default ActionTypes
這段代碼很好理解,就是對外暴露兩個action類型,沒什么難點。但是我這里想介紹的是Number.prototype.toString方法,估計應該有不少小伙伴們不知道toString是可以傳參的,toString接收一個參數radix,代表數字的基數,也就是我們所說的2進制、10進制、16進制等等。radix的取值范圍也很容易得出來,最小進制就是我們得二進制,所以redix>=2。0-9(10個數字)+a-z(26個英文字母)總共36個,所以redix<=36。總結一下2<=radix<=36,默認是10。基于這個特性我們可以寫一個獲取指定長度的隨機字符串的長度:
//獲取指定長度的隨機字符串 function randomString(length){ let str=""; while(length>0){ const fragment= Math.random().toString(36).substring(2); if(length>fragment.length){ str+=fragment; length-=fragment.length; }else{ str+=fragment.substring(0,length); length=0; } } return str; }isPlainObject.js
export default function isPlainObject(obj) { if (typeof obj !== "object" || obj === null) return false let proto = obj while (Object.getPrototypeOf(proto) !== null) { proto = Object.getPrototypeOf(proto) } return Object.getPrototypeOf(obj) === proto }
isPlainObject.js也很簡單,僅僅只是向外暴露了一個用于判斷是否簡單對象的函數。什么簡單對象?應該有一些小伙伴不理解,所謂的簡單對象就是該對象的__proto__等于Object.prototype,用一句通俗易懂的話就是:
凡不是new Object()或者字面量的方式構建出來的對象都不是簡單對象
下面看一個例子:
class Fruit{ sayName(){ console.log(this.name) } } class Apple extends Fruit{ constructor(){ super(); this.name="蘋果" } } const apple = new Apple(); const fruit = new Fruit(); const cherry = new Object({ name:"櫻桃" }); const banana = { name:"香蕉" }; console.log(isPlainObject(apple));//false console.log(isPlainObject(fruit));//false console.log(isPlainObject(cherry));//true console.log(isPlainObject(banana));//true
這里可能會有人不理解isPlainObject(fruit)===false,如果對這個不能理解的話,自己后面要補習一下原型鏈的相關知識,這里fruit.__proto__.__proto__才等價于Object.prototype。
warning.jsexport default function warning(message) { if (typeof console !== "undefined" && typeof console.error === "function") { console.error(message) } try { throw new Error(message) } catch (e) {} }
這個也很簡單,僅僅是打印一下錯誤信息。不過這里它的console居然加了一層判斷,我查閱了一下發現console其實是有兼容性問題,ie8及其以下都是不支持console的。哎,不僅感嘆一句!
如果說馬賽克阻礙了人類文明的進程,那ie便是阻礙了前端技術的發展。邏輯代碼
到這里我已經完成對utils下的js分析,很簡單,并沒有大家想象的那么難。僅僅從這幾個簡單的js中,就牽引出好幾個我們平時不太關注的知識點。如果我們不讀這些源碼,這些容易被忽視的知識點就很難被撿起來,這也是為什么很多大佬建議閱讀源碼的原因。我個人認為,閱讀源碼,理解原理是次要的。學習大佬的代碼風格、一些解決思路以及對自己知識盲點的點亮更為重要。廢話不多說,開始我們下一個部分的代碼閱讀,下面的部分就是整個redux的核心部分。
index.jsimport createStore from "./createStore" import combineReducers from "./combineReducers" import bindActionCreators from "./bindActionCreators" import applyMiddleware from "./applyMiddleware" import compose from "./compose" import warning from "./utils/warning" import __DO_NOT_USE__ActionTypes from "./utils/actionTypes" function isCrushed() {} if ( process.env.NODE_ENV !== "production" && typeof isCrushed.name === "string" && isCrushed.name !== "isCrushed" ) { warning( "You are currently using minified code outside of NODE_ENV === "production". " + "This means that you are running a slower development build of Redux. " + "You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify " + "or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) " + "to ensure you have the correct code for your production build." ) } export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose, __DO_NOT_USE__ActionTypes }
index.js是整個redux的入口文件,尾部的export出來的方法是不是都很熟悉,每個方法對應了一個js,這也是后面我們要分析的。這個有兩個點需要講一下:
第一個,__DO_NOT_USE__ActionTypes。 這個很陌生,平時在項目里面我們是不太會用到的,redux的官方文檔也沒有提到這個,如果你不看源碼你可能就不知道這個東西的存在。這個干嘛的呢?我們一點一點往上找,找到這么一行代碼:
import __DO_NOT_USE__ActionTypes from "./utils/actionTypes"
這個引入的js不就是我們之前分析的utils的其中一員嗎?里面定義了redux自帶的action的類型,從這個變量的命名來看,這是幫助開發者檢查不要使用redux自帶的action的類型,以防出現錯誤。
第二個,函數isCrushed。 這里面定義了一個函數isCrushed,但是函數體里面并沒有東西。第一次看的時候很奇怪,為啥要這么干?相信有不少小伙伴們跟我有一樣的疑問,繼續往下看,緊跟著后面有一段代碼:
if ( process.env.NODE_ENV !== "production" && typeof isCrushed.name === "string" && isCrushed.name !== "isCrushed" ) { warning( "You are currently using minified code outside of NODE_ENV === "production". " + "This means that you are running a slower development build of Redux. " + "You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify " + "or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) " + "to ensure you have the correct code for your production build." ) }
看到process.env.NODE_ENV,這里就要跟我們打包時用的環境變量聯系起來。當process.env.NODE_ENV==="production"這句話直接不成立,所以warning也就不會執行;當process.env.NODE_ENV!=="production",比如是我們的開發環境,我們不壓縮代碼的時候typeof isCrushed.name === "string" && isCrushed.name !== "isCrushed"也不會成立;當process.env.NODE_ENV!=="production",同樣是我們的開發環境,我們進行了代碼壓縮,此時isCrushed.name === "string" && isCrushed.name !== "isCrushed"就成立了,可能有人不理解isCrushed函數不是在的嗎?為啥這句話就不成立了呢?其實很好理解,了解過代碼壓縮的原理的人都知道,函數isCrushed的函數名將會被一個字母所替代,這里我們舉個例子,我將redux項目的在development環境下進行了一次壓縮打包。代碼做了這么一層轉換:
未壓縮
function isCrushed() {} if ( process.env.NODE_ENV !== "production" && typeof isCrushed.name === "string" && isCrushed.name !== "isCrushed" )
壓縮后
function d(){}"string"==typeof d.name&&"isCrushed"!==d.name
此時判斷條件就成立了,錯誤信息就會打印出來。這個主要作用就是防止開發者在開發環境下對代碼進行壓縮。開發環境下壓縮代碼,不僅讓我們
createStore.js函數createStore接受三個參數(reducer、preloadedState、enhancer),reducer和enhancer我們用的比較多,preloadedState用的比較少。第一個reducer很好理解,這里就不過多解釋了,第二個preloadedState,它代表著初始狀態,我們平時在項目里也很少用到它,主要說一下enhancer,中文名叫增強器,顧名思義就是來增強redux的,它的類型的是Function,createStore.js里有這么一行代碼:
if (typeof enhancer !== "undefined") { if (typeof enhancer !== "function") { throw new Error("Expected the enhancer to be a function.") } return enhancer(createStore)(reducer, preloadedState) }
這行代碼展示了enhancer的調用過程,根據這個調用過程我們可以推導出enhancer的函數體的架子應該是這樣子的:
function enhancer(createStore) { return (reducer,preloadedState) => { //邏輯代碼 ....... } }
常見的enhancer就是redux-thunk以及redux-saga,一般都會配合applyMiddleware一起使用,而applyMiddleware的作用就是將這些enhancer格式化成符合redux要求的enhancer。具體applyMiddleware實現,下面我們將會講到。我們先看redux-thunk的使用的例子:
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import rootReducer from "./reducers/index"; const store = createStore( rootReducer, applyMiddleware(thunk) );
看完上面的代碼,可能會有人有這么一個疑問“createStore函數第二個參數不是preloadedState嗎?這樣不會報錯嗎?” 首先肯定不會報錯,畢竟官方給的例子,不然寫個錯誤的例子也太大跌眼鏡了吧!redux肯定是做了這么一層轉換,我在createStore.js找到了這么一行代碼:
if (typeof preloadedState === "function" && typeof enhancer === "undefined") { enhancer = preloadedState preloadedState = undefined }
當第二個參數preloadedState的類型是Function的時候,并且第三個參數enhancer未定義的時候,此時preloadedState將會被賦值給enhancer,preloadedState會替代enhancer變成undefined的。有了這么一層轉換之后,我們就可以大膽地第二個參數傳enhancer了。
說完createStore的參數,下面我說一下函數createStore執行完之后返回的對象都有什么?在createStore.js最下面一行有這一行代碼:
return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable }
他返回了有這么幾個方法,其中前三個最為常用,后面兩個在項目基本上不怎么用,接下來我們去一一剖析。
定義的一些變量let currentState = preloadedState //從函數createStore第二個參數preloadedState獲得 let currentReducer = reducer //從函數createStore第一個參數reducer獲得 let currentListeners = [] //當前訂閱者列表 let nextListeners = currentListeners //新的訂閱者列表 let isDispatching = false
其中變量isDispatching,作為鎖來用,我們redux是一個統一管理狀態容器,它要保證數據的一致性,所以同一個時間里,只能做一次數據修改,如果兩個action同時觸發reducer對同一數據的修改,那么將會帶來巨大的災難。所以變量isDispatching就是為了防止這一點而存在的。
dispatchfunction dispatch(action) { if (!isPlainObject(action)) { throw new Error( "Actions must be plain objects. " + "Use custom middleware for async actions." ) } if (typeof action.type === "undefined") { throw new Error( "Actions may not have an undefined "type" property. " + "Have you misspelled a constant?" ) } if (isDispatching) { throw new Error("Reducers may not dispatch actions.") } try { isDispatching = true currentState = currentReducer(currentState, action) } finally { isDispatching = false } const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } return action }
函數dispatch在函數體一開始就進行了三次條件判斷,分別是以下三個:
判斷action是否為簡單對象
判斷action.type是否存在
判斷當前是否有執行其他的reducer操作
當前三個預置條件判斷都成立時,才會執行后續操作,否則拋出異常。在執行reducer的操作的時候用到了try-finally,可能大家平時try-catch用的比較多,這個用到的還是比較少。執行前isDispatching設置為true,阻止后續的action進來觸發reducer操作,得到的state值賦值給currentState,完成之后再finally里將isDispatching再改為false,允許后續的action進來觸發reducer操作。接著一一通知訂閱者做數據更新,不傳入任何參數。最后返回當前的action。
getStatefunction getState() { if (isDispatching) { throw new Error( "You may not call store.getState() while the reducer is executing. " + "The reducer has already received the state as an argument. " + "Pass it down from the top reducer instead of reading it from the store." ) } return currentState }
getState相比較dispatch要簡單許多,返回currentState即可,而這個currentState在每次dispatch得時候都會得到響應的更新。同樣是為了保證數據的一致性,當在reducer操作的時候,是不可以讀取當前的state值的。說到這里,我想到之前一次的面試經歷:
面試官:執行createStore函數生成的store,可不可以直接修改它的state? 我:可以。(普羅大眾的第一反應) 面試官:你知道redux怎么做到不能修改store的state嗎? 我:額......(處于懵逼狀態) 面試官:很簡單啊!重寫store的set方法啊!
那會沒看過redux的源碼,就被他忽悠了!讀完redux源碼之后,靠!這家伙就是個騙子!自己沒讀過源碼還跟我聊源碼,無語了!當然,我自己也有原因,學藝不精,被忽悠了。我們這里看了源碼之后,getState函數返回state的時候,并沒有對currentState做一層拷貝再給我們,所以是可以直接修改的。只是這么修改的話,就不會通知訂閱者做數據更新。得出的結論是:
store通過getState得出的state是可以直接被更改的,但是redux不允許這么做,因為這樣不會通知訂閱者更新數據。subscribe
function subscribe(listener) { if (typeof listener !== "function") { throw new Error("Expected the listener to be a function.") } if (isDispatching) { throw new Error( "You may not call store.subscribe() while the reducer is executing. " + "If you would like to be notified after the store has been updated, subscribe from a " + "component and invoke store.getState() in the callback to access the latest state. " + "See https://redux.js.org/api-reference/store#subscribe(listener) for more details." ) } let isSubscribed = true //表示該訂閱者在訂閱狀態中,true-訂閱中,false-取消訂閱 ensureCanMutateNextListeners() nextListeners.push(listener) return function unsubscribe() { if (!isSubscribed) { return } if (isDispatching) { throw new Error( "You may not unsubscribe from a store listener while the reducer is executing. " + "See https://redux.js.org/api-reference/store#subscribe(listener) for more details." ) } isSubscribed = false ensureCanMutateNextListeners() const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } }
在注冊訂閱者之前,做了兩個條件判斷:
判斷監聽者是否為函數
是否有reducer正在進行數據修改(保證數據的一致性)
接下來執行了函數ensureCanMutateNextListeners,下面我們看一下ensureCanMutateNextListeners函數的具體實現邏輯:
function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } }
邏輯很簡單,判斷nextListeners和currentListeners是否為同一個引用,還記得dispatch函數中有這么一句代碼以及定義變量時一行代碼嗎?
// Function dispatch const listeners = (currentListeners = nextListeners)
// 定義變量 let currentListeners = [] let nextListeners = currentListeners
這兩處將nextListeners和currentListeners引用了同一個數組,另外定義變量時也有這么一句話代碼。而ensureCanMutateNextListeners就是用來判斷這種情況的,當nextListeners和currentListeners為同一個引用時,則做一層淺拷貝,這里用的就是Array.prototype.slice方法,該方法會返回一個新的數組,這樣就可以達到淺拷貝的效果。
函數ensureCanMutateNextListeners作為處理之后,將新的訂閱者加入nextListeners中,并且返回取消訂閱的函數unsubscribe。函數unsubscribe執行時,也會執行兩個條件判斷:
是否已經取消訂閱(已取消的不必執行)
是否有reducer正在進行數據修改(保證數據的一致性)
通過條件判斷之后,講該訂閱者從nextListeners中刪除。看到這里可能有小伙伴們對currentListeners和nextListeners有這么一個疑問?函數dispatch里面將二者合并成一個引用,為啥這里有啥給他倆分開?直接用currentListeners不可以嗎?這里這樣做其實也是為了數據的一致性,因為有這么一種的情況存在。當redux在通知所有訂閱者的時候,此時又有一個新的訂閱者加進來了。如果只用currentListeners的話,當新的訂閱者插進來的時候,就會打亂原有的順序,從而引發一些嚴重的問題。
replaceReducerfunction replaceReducer(nextReducer) { if (typeof nextReducer !== "function") { throw new Error("Expected the nextReducer to be a function.") } currentReducer = nextReducer dispatch({ type: ActionTypes.REPLACE }) }
這個函數是用來替換reducer的,平時項目里基本很難用到,replaceReducer函數執行前會做一個條件判斷:
判斷所傳reducer是否為函數
通過條件判斷之后,將nextReducer賦值給currentReducer,以達到替換reducer效果,并觸發state更新操作。
observable/** * Interoperability point for observable/reactive libraries. * @returns {observable} A minimal observable of state changes. * For more information, see the observable proposal: * https://github.com/tc39/proposal-observable */
這里沒貼代碼,因為這塊代碼我們不需要掌握。這個observable函數,并沒有調用,即便暴露出來我們也辦法使用。所以我們就跳過這塊,如果有興趣的話,可以去作者給的github的地址了解一下。
講完這幾個方法之后,還有一個小細節需要說一下,createStore函數體里有這樣一行代碼。
dispatch({ type: ActionTypes.INIT })
為啥要有這么一行代碼?原因很簡單,假設我們沒有這樣代碼,此時currentState就是undefined的,也就我說我們沒有默認值了,當我們dispatch一個action的時候,就無法在currentState基礎上做更新。所以需要拿到所有reducer默認的state,這樣后續的dispatch一個action的時候,才可以更新我們的state。
combineReducers.js這個js對應著redux里的combineReducers方法,主要作用就是合并多個reducer。現在我們先給一個空的函數,然后再一步步地根據還原源碼,這樣大家可能理解得更為透徹點。
//reducers Object類型 每個屬性對應的值都要是function export default function combineReducers(reducers) { .... }第一步:淺拷貝reducers
export default function combineReducers(reducers) { const reducerKeys = Object.keys(reducers) const finalReducers = {} for (let i = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i] if (process.env.NODE_ENV !== "production") { if (typeof reducers[key] === "undefined") { warning(`No reducer provided for key "${key}"`) } } if (typeof reducers[key] === "function") { finalReducers[key] = reducers[key] } } const finalReducerKeys = Object.keys(finalReducers) }
這里定義了一個finalReducers和finalReducerKeys,分別用來拷貝reducers和其屬性。先用Object.keys方法拿到reducers所有的屬性,然后進行for循環,每一項可根據其屬性拿到對應的reducer,并淺拷貝到finalReducers中,但是前提條件是每個reducer的類型必須是Function,不然會直接跳過不拷貝。
第二步:檢測finalReducers里的每個reducer是否都有默認返回值function assertReducerShape(reducers) { Object.keys(reducers).forEach(key => { const reducer = reducers[key] const initialState = reducer(undefined, { type: ActionTypes.INIT }) if (typeof initialState === "undefined") { throw new Error( `Reducer "${key}" returned undefined during initialization. ` + `If the state passed to the reducer is undefined, you must ` + `explicitly return the initial state. The initial state may ` + `not be undefined. If you don"t want to set a value for this reducer, ` + `you can use null instead of undefined.` ) } const type = "@@redux/PROBE_UNKNOWN_ACTION_" + Math.random() .toString(36) .substring(7) .split("") .join(".") if (typeof reducer(undefined, { type }) === "undefined") { throw new Error( `Reducer "${key}" returned undefined when probed with a random type. ` + `Don"t try to handle ${ ActionTypes.INIT } or other actions in "redux/*" ` + `namespace. They are considered private. Instead, you must return the ` + `current state for any unknown actions, unless it is undefined, ` + `in which case you must return the initial state, regardless of the ` + `action type. The initial state may not be undefined, but can be null.` ) } }) } export default function combineReducers(reducers) { //省略第一步的代碼 ...... let shapeAssertionError try { assertReducerShape(finalReducers) } catch (e) { shapeAssertionError = e } }
assertReducerShape方法主要檢測兩點:
不能占用
如果遇到未知的action的類型,不需要要用默認返回值
如果傳入type為 @@redux/INIT<隨機值> 的action,返回undefined,說明沒有對未
知的action的類型做響應,需要加默認值。如果對應type為 @@redux/INIT<隨機值> 的action返回不為undefined,但是卻對應type為 @@redux/PROBE_UNKNOWN_ACTION_<隨機值> 返回為undefined,說明占用了
export default function combineReducers(reducers) { //省略第一步和第二步的代碼 ...... let unexpectedKeyCache if (process.env.NODE_ENV !== "production") { unexpectedKeyCache = {} } return function combination(state = {}, action) { if (shapeAssertionError) { throw shapeAssertionError } if (process.env.NODE_ENV !== "production") { const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, unexpectedKeyCache ) if (warningMessage) { warning(warningMessage) } } let hasChanged = false const nextState = {} for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i] const reducer = finalReducers[key] const previousStateForKey = state[key] const nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === "undefined") { const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey !== previousStateForKey } return hasChanged ? nextState : state } }
首先對傳入的state用getUnexpectedStateShapeWarningMessage做了一個異常檢測,找出state里面沒有對應reducer的key,并提示開發者做調整。接著我們跳到getUnexpectedStateShapeWarningMessage里,看其實現。
function getUnexpectedStateShapeWarningMessage( inputState, reducers, action, unexpectedKeyCache ) { const reducerKeys = Object.keys(reducers) const argumentName = action && action.type === ActionTypes.INIT ? "preloadedState argument passed to createStore" : "previous state received by the reducer" if (reducerKeys.length === 0) { return ( "Store does not have a valid reducer. Make sure the argument passed " + "to combineReducers is an object whose values are reducers." ) } if (!isPlainObject(inputState)) { return ( `The ${argumentName} has unexpected type of "` + {}.toString.call(inputState).match(/s([a-z|A-Z]+)/)[1] + `". Expected argument to be an object with the following ` + `keys: "${reducerKeys.join("", "")}"` ) } const unexpectedKeys = Object.keys(inputState).filter( key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key] ) unexpectedKeys.forEach(key => { unexpectedKeyCache[key] = true }) if (action && action.type === ActionTypes.REPLACE) return if (unexpectedKeys.length > 0) { return ( `Unexpected ${unexpectedKeys.length > 1 ? "keys" : "key"} ` + `"${unexpectedKeys.join("", "")}" found in ${argumentName}. ` + `Expected to find one of the known reducer keys instead: ` + `"${reducerKeys.join("", "")}". Unexpected keys will be ignored.` ) } }
getUnexpectedStateShapeWarningMessage接收四個參數 inputState(state)、reducers(finalReducers)、action(action)、unexpectedKeyCache(unexpectedKeyCache),這里要說一下unexpectedKeyCache是上一次檢測inputState得到的其里面沒有對應的reducer集合里的異常key的集合。整個邏輯如下:
前置條件判斷,保證reducers集合不為{}以及inputState為簡單對象
找出inputState里有的key但是 reducers集合里沒有key
如果是替換reducer的action,跳過第四步,不打印異常信息
將所有異常的key打印出來
getUnexpectedStateShapeWarningMessage分析完之后,我們接著看后面的代碼。
let hasChanged = false const nextState = {} for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i] const reducer = finalReducers[key] const previousStateForKey = state[key] const nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === "undefined") { const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey !== previousStateForKey } return hasChanged ? nextState : state
首先定義了一個hasChanged變量用來表示state是否發生變化,遍歷reducers集合,將每個reducer對應的原state傳入其中,得出其對應的新的state。緊接著后面對新的state做了一層未定義的校驗,函數getUndefinedStateErrorMessage的代碼如下:
function getUndefinedStateErrorMessage(key, action) { const actionType = action && action.type const actionDescription = (actionType && `action "${String(actionType)}"`) || "an action" return ( `Given ${actionDescription}, reducer "${key}" returned undefined. ` + `To ignore an action, you must explicitly return the previous state. ` + `If you want this reducer to hold no value, you can return null instead of undefined.` ) }
邏輯很簡單,僅僅做了一下錯誤信息的拼接。未定義校驗完了之后,會跟原state作對比,得出其是否發生變化。最后發生變化返回nextState,否則返回state。
compose.js這個函數主要作用就是將多個函數連接起來,將一個函數的返回值作為另一個函數的傳參進行計算,得出最終的返回值。以烹飪為例,每到料理都是從最初的食材經過一道又一道的工序處理才得到的。compose的用處就可以將這些烹飪工序連接到一起,你只需要提供食材,它會自動幫你經過一道又一道的工序處理,烹飪出這道料理。
export default function compose(...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args) => a(b(...args))) }
上面是es6的代碼,可能小伙伴們并不是很好理解,為了方便大家理解,我將其轉換成es5代碼去做講解。
function compose() { var _len = arguments.length; var funcs = []; for (var i = 0; i < _len; i++) { funcs[i] = arguments[i]; } if (funcs.length === 0) { return function (arg) { return arg; }; } if (funcs.length === 1) { return funcs[0]; } return funcs.reduce(function (a, b) { return function () { return a(b.apply(undefined, arguments)); }; }); }
梳理一下整個流程,大致分為這么幾步:
新建一個新數組funcs,將arguments里面的每一項一一拷貝到funcs中去
當funcs的長度為0時,返回一個傳入什么就返回什么的函數
當funcs的長度為1時,返回funcs第0項對應的函數
當funcs的長度大于1時,調用Array.prototype.reduce方法進行整合
這里我們正好復習一下數組的reduce方法,函數reduce接受下面四個參數
total 初始值或者計算得出的返回值
current 當前元素
index 當前元素的下標
array 當前元素所在的數組
示例:
const array = [1,2,3,4,5,6,7,8,9,10]; const totalValue=array.reduce((total,current)=>{ return total+current }); //55
這里的compose有個特點,他不是從左到右執行的,而是從右到左執行的,下面我們看個例子:
const value=compose(function(value){ return value+1; },function(value){ return value*2; },function(value){ return value-3; })(2); console.log(value);//(2-3)*2+1=-1
如果想要其從左向右執行也很簡單,做一下順序的顛倒即可。
===> 轉換前 return a(b.apply(undefined, arguments)); ===> 轉換后 return b(a.apply(undefined, arguments));applyMiddleware.js
export default function applyMiddleware(...middlewares) { return createStore => (...args) => { const store = createStore(...args) let dispatch = () => { throw new Error( `Dispatching while constructing your middleware is not allowed. ` + `Other middleware would not be applied to this dispatch.` ) } const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } }
前面我們講enhancer的時候,提到過這個applyMiddleware,現在我們將二者的格式對比看一下。
// enhancer function enhancer(createStore) { return (reducer,preloadedState) => { //邏輯代碼 ....... } } //applyMiddleware function //applyMiddleware(...middlewares) { return createStore => (...args) => { //邏輯代碼 ....... } }
通過二者的對比,我們發現函數applyMiddleware的返回就是一個enhancer,下面我們再看其具體實現邏輯:
通過createStore方法創建出一個store
定一個dispatch,如果在中間件構造過程中調用,拋出錯誤提示
定義middlewareAPI,有兩個方法,一個是getState,另一個是dispatch,將其作為中間件調用的store的橋接
middlewares調用Array.prototype.map進行改造,存放在chain
用compose整合chain數組,并賦值給dispatch
將新的dispatch替換原先的store.dispatch
看完整個過程可能小伙伴們還是一頭霧水,玄學的很!不過沒關系,我們以redux-thunk為例,模擬一下整個過程中,先把redux-thunk的源碼貼出來:
function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { if (typeof action === "function") { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
哈哈哈!看完redux-thunk的源碼之后是不是很奔潰,幾千star的項目居然就幾行代碼,頓時三觀就毀了有木有?其實源碼沒有大家想象的那么復雜,不要一聽源碼就慌。穩住!我們能贏!根據redux-thunk的源碼,我們拿到的thunk應該是這樣子的:
const thunk = ({ dispatch, getState })=>{ return next => action => { if (typeof action === "function") { return action(dispatch, getState); } return next(action); }; }
我們經過applyMiddleware處理一下,到第四步的時候,chain數組應該是這樣子的:
const newDispatch; const middlewareAPI={ getState:store.getState, dispatch: (...args) => newDispatch(...args) } const { dispatch, getState } = middlewareAPI; const fun1 = (next)=>{ return action => { if (typeof action === "function") { return action(dispatch, getState); } return next(action); } } const chain = [fun1]
compose整合完chain數組之后得到的新的dispatch的應該是這樣子:
const newDispatch; const middlewareAPI={ getState:store.getState, dispatch: (...args) => newDispatch(...args) } const { dispatch, getState } = middlewareAPI; const next = store.dispatch; newDispatch = action =>{ if (typeof action === "function") { return action(dispatch, getState); } return next(action); }
接下來我們可以結合redux-thunk的例子來模擬整個過程:
function makeASandwichWithSecretSauce(forPerson) { return function (dispatch) { return fetchSecretSauce().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize("The Sandwich Shop", forPerson, error)) ); }; } // store.dispatch就等價于newDispatch store.dispatch(makeASandwichWithSecretSauce("Me")) ====> 轉換 const forPerson = "Me"; const action = (dispatch)=>{ return fetchSecretSauce().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize("The Sandwich Shop", forPerson, error)) ); } newDispatch() ===> typeof action === "function" 成立時 ((dispatch)=>{ return fetchSecretSauce().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize("The Sandwich Shop", forPerson, error)) ); })( (...args) => newDispatch(...args), getState) ====> 計算運行結果 const forPerson = "Me"; const dispatch = (...args) => newDispatch(...args) ; fetchSecretSauce().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize("The Sandwich Shop", forPerson, error)) ); // 其中: function fetchSecretSauce() { return fetch("https://www.google.com/search?q=secret+sauce"); } function makeASandwich(forPerson, secretSauce) { return { type: "MAKE_SANDWICH", forPerson, secretSauce }; } function apologize(fromPerson, toPerson, error) { return { type: "APOLOGIZE", fromPerson, toPerson, error }; } ====> 我們這里只計算Promise.resolve的結果,并且假設fetchSecretSauce返回值為"666",即sauce="666" const forPerson = "Me"; const dispatch = (...args) => newDispatch(...args) ; dispatch({ type: "MAKE_SANDWICH", "Me", "666" }) ====> 為了方便對比,我們再次轉換一下 const action = { type: "MAKE_SANDWICH", "Me", "666" }; const next = store.dispatch const newDispatch = action =>{ if (typeof action === "function") { return action(dispatch, getState); } return next(action); } newDispatch(action) ====> 最終結果 store.dispatch({ type: "MAKE_SANDWICH", "Me", "666" });
以上就是redux-thunk整個流程,第一次看肯能依舊會很懵,后面可以走一遍,推導一下加深自己的理解。
bindActionCreators.jsexport default function bindActionCreators(actionCreators, dispatch) { if (typeof actionCreators === "function") { return bindActionCreator(actionCreators, dispatch) } if (typeof actionCreators !== "object" || actionCreators === null) { throw new Error( `bindActionCreators expected an object or a function, instead received ${ actionCreators === null ? "null" : typeof actionCreators }. ` + `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?` ) } const keys = Object.keys(actionCreators) const boundActionCreators = {} for (let i = 0; i < keys.length; i++) { const key = keys[i] const actionCreator = actionCreators[key] if (typeof actionCreator === "function") { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators }
bindActionCreators針對于三種情況有三種返回值,下面我們根據每種情況的返回值去分析。(為了方便理解,我們選擇在無集成中間件的情況)
typeof actionCreators === "function"function bindActionCreator(actionCreator, dispatch) { return function() { return dispatch(actionCreator.apply(this, arguments)) } } const actionFun=bindActionCreator(actionCreators, dispatch) ===> 整合一下 const fun1 = actionCreators; const dispatch= stror.dispatch; const actionFun=function () { return dispatch(fun1.apply(this, arguments)) }
根據上面的推導,當變量actionCreators的類型為Function時,actionCreators必須返回一個action。
typeof actionCreators !== "object" || actionCreators === nullthrow new Error( `bindActionCreators expected an object or a function, instead received ${ actionCreators === null ? "null" : typeof actionCreators }. ` + `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?` )
提示開發者actionCreators類型錯誤,應該是一個非空對象或者是函數。
默認const keys = Object.keys(actionCreators) const boundActionCreators = {} for (let i = 0; i < keys.length; i++) { const key = keys[i] const actionCreator = actionCreators[key] if (typeof actionCreator === "function") { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators
通過和第一種情況對比發現,當actionCreators的每一項都執行一次第一種情況的操作。換句話說,默認情況是第一種情況的集合。
以上是對bindActionCreators的剖析,可能小伙伴們對這個還是不夠理解,不過沒有關系,只要知道bindActionCreators干了啥就行。bindActionCreators是需要結合react-redux一起使用的,由于本篇文章沒有講解react-redux,所以這里我們不對bindActionCreators做更深入的講解。下篇文章講react-redux,會再次提到bindActionCreators。
結語到這里整個redux的源碼我們已經剖析完了,整個redux代碼量不是很大,但是里面的東西還是很多的,邏輯相對來說有點繞。不過沒關系,沒有什么是看了好幾次都看不懂的,如果有那就再多看幾次嘛!另外再多一嘴,如果想快讀提高自己的小伙伴們,我個人是強烈推薦看源碼的。正所謂“近朱者赤,近墨者黑”,多看看大神的代碼,對自己的代碼書寫、代碼邏輯、知識點查缺補漏等等方面都是很大幫助的。就拿我自己來說,我每次閱讀完一篇源碼之后,都受益匪淺。可能第一次看源碼,有著諸多的不適應,畢竟萬事開頭難,如果強迫自己完成第一次的源碼閱讀,那往后的源碼閱讀將會越來越輕松,對自己的提升也就越來越快。各位騷年們,擼起袖子加油干吧!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/97927.html
摘要:本文共字,閱讀大約需要分鐘概述在前文字符串類型內部編碼剖析之中已經剖析過最基本的類型的內部是怎么編碼和存儲的,本文再來闡述中使用最為頻繁的數據類型哈希或稱散列,在內部是怎么存的。 showImg(https://segmentfault.com/img/remote/1460000016158153); 本文共 1231字,閱讀大約需要 5分鐘 ! 概述 在前文《Redis字符串類型...
閱讀 661·2021-11-15 11:39
閱讀 2898·2021-10-08 10:04
閱讀 3261·2019-08-30 10:57
閱讀 3023·2019-08-26 13:25
閱讀 1904·2019-08-26 12:14
閱讀 2635·2019-08-23 15:27
閱讀 2993·2019-08-23 15:18
閱讀 1774·2019-08-23 14:26