摘要:循環還沒有結束,其中的某個對進行了添加或者刪除,都會影響到此次循環的進行,帶來不可預期的錯誤。
首先來一段 redux 結合 中間件 thunk、logger 的使用demo 了解一下應該如何使用
const redux = require("redux") const { createStore, combineReducers, bindActionCreators, compose, applyMiddleware } = redux const simpleState = { num: 0 } const complexState = { num: 1 } // logger中間件負責打印日志 function logger({dispatch, getState}) { return function(next) { return function(action) { console.log("logger start:" + action.type) const res = next(action) console.log("logger end:" + action.type) return res } } } // thunk中間件負責異步 function thunk({dispatch, getState}) { return function(next) { return function(action) { if (typeof action === "function") { return action(dispatch) } return next(action) } } } // 派發的增加方法 function increment(num) { return { type: "increment", payload: num } } // 派發的減少的方法 function decrement(num) { return { type: "decrement", payload: num } } // 派發的乘法的方法 function multiply(num) { return { type: "multiply", payload: num } } // 派發的除法的方法 function divide(num) { return { type: "divide", payload: num } } // 派發異步事件方法 function syncDivide() { return (dispatch) => { setTimeout(() => { dispatch(divide(10)) }, 2000) } } // 負責加減法的reducer function simpleCalcReducer(state = simpleState, action) { switch (action.type) { case "increment": return Object.assign({}, state, { num: state.num + action.payload }) case "decrement": return Object.assign({}, state, { num: state.num - action.payload }) default: return state } } // 負責乘除法的reducer function complexCalcReducer(state = complexState, action) { switch (action.type) { case "multiply": return Object.assign({}, state, { num: state.num * action.payload }) case "divide": return Object.assign({}, state, { num: state.num / action.payload }) default: return state } } const middleware = applyMiddleware(thunk, logger) const allReducers = combineReducers({ simpleCalcReducer, complexCalcReducer }) const store = createStore(allReducers, middleware) // 所有action const allActions = { increment, decrement, multiply, divide, syncDivide } const actions = bindActionCreators(allActions, store.dispatch) // 派發action actions.increment(2); actions.decrement(1); actions.multiply(10); actions.divide(5); actions.syncDivide() setTimeout(() => { console.log(store.getState()) }, 2500)
打印結果如下:
上面只是簡單使用了redux的部分api,來盡量實現一個標準項目中所使用到的redux的基本操作流程,下面進入源碼的分析階段
1.項目結構
第一張圖,先來看一下 redux 的項目結構目錄
---src
------utils // 工具函數庫
---------actionTypes.js // redux用來初始化state,自身調用的action
---------isPlainObject.js // 用來檢測action 是否為一個明確的對象
---------warning.js // 用來進行警告的公共方法
------applyMiddleware.js // 中間件,負責對store.dispatch方法進行包裝
------bindActionCreators.js // 對派發action的語法進行優化,使之可以更好的應用于react或其他
------combineReducers.js // 將分離的reducer合并成一個對象,并返回一個執行這些reducer的函數
------compose.js // 合并函數, 將函數參數按照從左向右的順序進行合并,返回一個新函數
------createStore.js // 核心方法,返回一個store對象,用來獲取state、派發action、添加訂閱者subscribe 等
------index.js // 入口文件,對外導出的方法
2.utils 工具方法的分析
將utils的三個方法放在最前面來分析,主要是為了先對三個工具方法有個簡單概念和了解,這樣在閱讀下面的內容時,對于有用到工具方法的地方可以直接理解,不用再打斷思緒去分析工具方法,影響整體的閱讀體驗
① warning.js -- 控制臺打印異常日志
export default function warning(message) { if (typeof console !== "undefined" && typeof console.error === "function") { console.error(message) } try { throw new Error(message) } catch (e) {} }
總結:
warining 函數非常簡單,當 console.error 方法存在時來打印 錯誤的 message,這里的一層判斷是為了兼容ie6789瀏覽器只有在開啟控制臺的情況下,console對象才會創建,否則會報console未定義而導出程序無法進行。
至于下面的為什么要 try 里面去拋出異常,本身這樣做對程序是沒有什么影響的,這樣的意義又是什么呢?源碼的注釋里解釋道 “This error was thrown as a convenience so that if you enable "break on all exceptions" in your console, it would pause the execution at this line ”,翻譯過來就是,拋出這個錯誤是為了方便查看錯誤源。只要我們開啟了斷點異常,那么程序就會在拋出錯誤的那行代碼上打上斷點,來方便進行調試和追蹤。那么在谷歌里面這個異常怎么開啟呢?F12打開谷歌的開發者工具,1點擊 Sources - 2點擊藍色的pause icon - 3勾選 Pause on caught exceptions,如下圖所示
在控制臺里測試如下代碼
鍵入回車后,瀏覽器出現斷點,跳轉至sources資源文件,并高亮了拋出錯誤的那行代碼,非常方便
② 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 }
總結:
Object.getPrototypeOf 方法用來返回對象的隱式原型,即構造這個對象的構造函數的原型。
使用 while 循環來查找目標對象的原型鏈頂層,因為 Object.prototype.__proto__ === null,所以Object.prototype 就是原型鏈的頂層,查找到最后一層時,proto 一定被賦值為 Object.prototype。
這樣做的意義是什么? Array數據類型 或者 dom的 document等數據類型的typeof 也是 object, 他們都不是直接由Object 函數來構建的對象,以 Array 數組的構造函數來舉例。
var ary = new Array() var pro = Object.getPrototypeOf(ary) console.log(pro === ary.__proto__) // true console.log(ary.__proto__ === Array.prototype) // true var pro2 = Object.getPrototypeOf(pro) console.log(pro2 === Object.prototype) // true Object.prototype.__proto__ === null // true
可見 ary 第一次獲取到的原型是 Array.prototype,而 Array.prototype 本身也是一個對象,必然由 Object 函數創建,所以 Array.prototype.__proto__ 又指向了 Object.prototype,到此循環結束。最終 pro = Object.prototype。這就造成了最終的 Object.getPrototypeOf(obj) 和 proto 是不等的。
所以這個方法的就很清晰了,只有直接使用 Object 函數創建的對象才會被判斷為真,因為只有它原型鏈存在一層
③ actionTypes.js -- redux 內部做初始化state的action
const randomString = () => Math.random() .toString(36) .substring(7) .split("") .join(".") const ActionTypes = { INIT: `@@redux/INIT${randomString()}`, REPLACE: `@@redux/REPLACE${randomString()}`, PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}` } export default ActionTypes
總結:
randomString() 方法隨機生成一個36進制的數字,然后切割拼接,最終生成 和"5.b.p.3.b.8" 格式一樣的字符串
這個方法 導出了一個對象,對象包含3個key: INIT、REPLACE、PROBE_UNKNOWN_ACTION,前兩個是字符串,后面一個是方法,方法也是返回一個拼接好的字符串,其實這三個都是redux內部用來派發action 的 type
3.模塊分析(去掉注釋)
① 入口文件 index.js**
import 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 setting mode to production in webpack (https://webpack.js.org/concepts/mode/) " + "to ensure you have the correct code for your production build." ) } export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose, __DO_NOT_USE__ActionTypes }
總結:
入口文件作用就時將幾個模塊文件引入和導出,里面有一個空的 isCrushed 函數,這個函數的意義就是判斷當前的構建工具是否為 node 環境,如果是 node 環境并且非生產環境,那么就要判斷當前的 redux 文件有沒有被壓縮。
判斷的目的就是希望開發者,在非生產環境下不要壓縮代碼,如果項目比較大,我們只是修改了一個小小的樣式,這時候如果開啟代碼的混淆壓縮,那么我們項目的所有依賴的文件都會被混淆壓縮,項目越大被壓縮的內容越多耗時越長,從而導致調試的時間增加,降低開發效率。這也正是redux在 warning 警告里提到的 "This means that you are running a slower development build of Redux",你正在一個緩慢的開發環境下使用 redux。
② 核心文件 creteStore.js
cretaeStore.js 是redux的核心文件,在這個方法里,redux 向外提供了 dispatch、subscribe、getState、replaceReducer 這四個核心方法。此外還有一個 [$$observable] 方法,這個方法并不是很好理解他的作用和意義,放在文章最后來說明。下面是移除了注釋的源代碼
import $$observable from "symbol-observable" import ActionTypes from "./utils/actionTypes" import isPlainObject from "./utils/isPlainObject" export default function createStore(reducer, preloadedState, enhancer) { // 判斷1 if ( (typeof preloadedState === "function" && typeof enhancer === "function") || (typeof enhancer === "function" && typeof arguments[3] === "function") ) { throw new Error( "It looks like you are passing several store enhancers to " + "createStore(). This is not supported. Instead, compose them " + "together to a single function." ) } // 判斷2 if (typeof preloadedState === "function" && typeof enhancer === "undefined") { enhancer = preloadedState preloadedState = undefined } // 判斷3 if (typeof enhancer !== "undefined") { if (typeof enhancer !== "function") { throw new Error("Expected the enhancer to be a function.") } return enhancer(createStore)(reducer, preloadedState) } // 判斷4 if (typeof reducer !== "function") { throw new Error("Expected the reducer to be a function.") } // 內部變量 let currentReducer = reducer let currentState = preloadedState let currentListeners = [] let nextListeners = currentListeners let isDispatching = false function ensureCanMutateNextListeners() { // ... } function getState() { // ... } function subscribe(listener) { // ... } function dispatch(action) { // ... } function replaceReducer(nextReducer) { // ... } function observable() { // ... } dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } }
createStore 方法接收3個參數,分別是 reducer,preloadedState,enhancer。
1.reducer 就是一個純函數用來返回 state
2.preloadedState 是初始化的state,在實際開發中,很少有傳遞這個這個參數。其實這個參數就是為了初始化必須的原始數據。此外,如果使用了combineReducer這個方法來組合多個reducer,相應的preloadedState 里的key 也必須要和 combineReducer 中的key相對應
3.enhancer 翻譯過來就是增強器,它是一個類似的高階函數用來包裝和增強 creteStore 內部的 dispatch、subscribe、getState 等方法,通過這個高階函數可以實現 中間件、時間旅行、持久化state等,在redux內只實現了一個enhancer,它就是中間件 applyMIddleware,用來強化 dispatch方法。
講完三個參數,開始解釋代碼
if ( (typeof preloadedState === "function" && typeof enhancer === "function") || (typeof enhancer === "function" && typeof arguments[3] === "function") ) { throw new Error( "It looks like you are passing several store enhancers to " + "createStore(). This is not supported. Instead, compose them " + "together to a single function." ) }
判斷1:提示很明確,當使用多個enhancer時,需要使用compose 方法將多個enhancer合并成一個單一的函數。
// 判斷2 if (typeof preloadedState === "function" && typeof enhancer === "undefined") { enhancer = preloadedState preloadedState = undefined }
判斷2:個人感覺這里的判斷就是為了符合更大眾化的開發需求,preloadedState 這個參數在實際開發中,真的很少傳遞。所以在這里做了一個判斷,如果開發中沒有用到這個初始的preloadedState,完全可以將它省略掉,直接傳遞最后一個enhancer函數,redux在內部幫開發者,完成了這部分參數轉換的處理。
// 判斷3 if (typeof enhancer !== "undefined") { if (typeof enhancer !== "function") { throw new Error("Expected the enhancer to be a function.") } return enhancer(createStore)(reducer, preloadedState) }
判斷3:當enhancer 存在并且為函數的時候,直接將當前creteStore方法作為參數傳遞給 enhancer。而這個enhancer(createStore),返回的就是 createStore 的加強版可以稱他為 creteStore-X,至于如何增強先放到后面的applyMiddleware這個enhancer來說明。這里先只需要知道,通過enhancer包裝過后的 createStore,內部的某些方法被加強了。
// 判斷4 if (typeof reducer !== "function") { throw new Error("Expected the reducer to be a function.") }
判斷4:reducer 必須是一個函數 函數 函數
let currentReducer = reducer let currentState = preloadedState let currentListeners = [] let nextListeners = currentListeners let isDispatching = false
內部變量:
這里分別使用了,currentReducer 和 currentState 來接收初始傳遞的 reducer 和 preloadedState,重新賦值是因為 currentReducer 和 currentState 都是可變的,當他們被修改的時候不會影響初始的reducer 和 preloadedState。
currentListeners 是一個數組用來存儲訂閱的函數列表,為什么還要多定義一個 nextListeners = currentListeners 呢?這個問題放到后面看比較好理解,先掠過
isDispatching 比較好理解,用來判斷redux是否處于派發action的狀態中,即是否在執行reducer。
function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } }
ensureCanMutateNextListeners:返回currentListeners 的淺拷貝
這里的意圖不是很明顯,當nextListeners 和 currentListeners 全等的時候,返回一個 currentListeners 的 淺拷貝賦值給 nextListenenrs,意義是什么呢接著向下看。
function 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:返回當前的currentState
返回當前的currentState,當action還在 派發當中時,如果調用這個方法會拋出錯誤
function subscribe(listener) { // 被添加的訂閱者必須是一個函數 if (typeof listener !== "function") { throw new Error("Expected the listener to be a function.") } // 處于dispatch的時候,不允許添加訂閱者 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." ) } // 這里是一個閉包,isSubscribed 用來表示當前的訂閱者已經成功的被 // 添加到了訂閱的列表中 let isSubscribed = true // 當nexListeners 和 currentListeners 全等時候,返回了一個新的 // nextListeners, 然后將訂閱者添加到新的 nextListeners ensureCanMutateNextListeners() // ?1 nextListeners.push(listener) // 返回一個 unsubscribe, 用來取消已經添加到訂閱列表中的訂閱者 return function unsubscribe() { // 如果當前的訂閱者已經被取消了 直接返回 if (!isSubscribed) { return } // 處于dispatch的時候,不允許取消訂閱者 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." ) } // 將訂閱者的狀態改為false isSubscribed = false // 繼續更新 nextListeners ensureCanMutateNextListeners() // 將取消的訂閱者從 nextListeners 中刪除 const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } }
subscribe:用來添加和刪除訂閱者
這樣粗略看下來似乎也沒太看清楚這個 ensureCanMutateNextListeners 方法有什么卵用,繼續翻看redux 對ensureCanMutateNextListeners 方法的解釋,里面提到
"This makes a shallow copy of currentListeners so we can use nexListeners as a temporary list while dispatching"
"This prevents any bugs around consumers calling subscribe/unsubscribe in the middle of a dispatch"
主要就是后面這一句,這個方法 可以防止用戶在執行dispatch中調用訂閱或者取消訂閱時出現任何錯誤。還是不明所以,繼續向下尋找答案
function dispatch(action) { // 判斷 action 是否為明確的 object 對象 if (!isPlainObject(action)) { throw new Error( "Actions must be plain objects. " + "Use custom middleware for async actions." ) } // action 必須有 type 這個字段類型 if (typeof action.type === "undefined") { throw new Error( "Actions may not have an undefined "type" property. " + "Have you misspelled a constant?" ) } // 不允許同時執行多個 dispatch if (isDispatching) { throw new Error("Reducers may not dispatch actions.") } // 將執行狀態設置為true, 開始執行reudcer, 并接收 currentState try { isDispatching = true currentState = currentReducer(currentState, action) } finally { // 無論成功失敗 都將執行狀態重置 isDispatching = false } // reducer 執行完畢之后,開始循環執行 訂閱者數組列表 const listeners = (currentListeners = nextListeners) // ?2 for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] // ?3 listener() } return action }
dispatch:用來派發action,執行reducer
問題2,每次執行訂閱者列表的時候,為什么要新定義一個 listeners 來接收 nextListeners,直接 currentListeners = nextListeners 然后使用 currentListeners 來循環不行嗎?
問題3,循環里面為什么不直接使用 listeners[i]() 來執行調用,而是定義一個變量來接收然后再執行?
再結合上面 subscribe 中 ensureCanMutateNextListeners 的到底有何意義?
其實,這一切的細節都是為了解決一種特殊的應用場景,在訂閱事件內部 再次 添加訂閱或者取消訂閱。如
store.subscribe( () => { store.subscribe(...) })
如果是這種場景,因為是在循環中去觸發 listener,單純的使用 currentListeners 來存儲訂閱列表是無法滿足的。循環還沒有結束,其中的某個 listener 對 currentListeners 進行了添加或者刪除,都會影響到此次循環的進行,帶來不可預期的錯誤。至此這些細節就變得清晰起來了。
引用官方的注釋更為準確,每次dispatch后,循環使用的是當前nextListeners 的快照,同樣也就是 currentListeners,它在循環結束之前是不會被改變的。想象一下,假如在訂閱事件的內部繼續調用 store.subsrcibe 來添加訂閱者,那么就會調用 ensureCanMutateNextListeners 這個方法,如果currentListeners 和 nextListeners 是完全相等的說明nextListeners 還未被改變,此時淺拷貝一份 currentListenrs 的隊列賦值為 nextListeners,nextListeners 就變成了一個全新的訂閱隊列,然后將 添加的訂閱者放到新的 nextListeners,這樣就完全不會影響到之前已經開始的循環。當下次disptach 再次發起的時候,將 currentListeners 同步為最新的 nextListeners 隊列。
問題 2 應該如何理解呢?找到了早期redux的提交記錄如下:
這里還沒有對 currentListeners 和 nextListeners 做概念的區分,只是將每次listeners 淺拷貝了一層用來 安全的執行循環。所以 const listeners = (currentListeners = nextListeners) 中聲明的 listeners并不是必須的,他的存在只是為了在之后在循環中使用 listeners 代替 currentListeners 少打幾個字母而已
問題 2 應該如何理解呢?其實在這里是為了確保在 listener 當中的 this 與我們對 js 當中的函數內部 this 指向誰的預期保持一致。這里就是將 this 重新綁定為默認的全局對象,如果直接使用 listeners[i]() 來調用,那么其內部的this 變指向了listeners 這個數組本身。
function replaceReducer(nextReducer) { if (typeof nextReducer !== "function") { throw new Error("Expected the nextReducer to be a function.") } currentReducer = nextReducer dispatch({ type: ActionTypes.REPLACE }) }
replaceReducer:替換reducer,并且重新 dispatch
replaceReducer 這個方法比較簡單,當我們的項目啟用了模塊懶加載,我們的最開始的reducer可能只是部分的模塊的reducer,這時候如果要引入新的模塊就需要能夠動態的替換 reducer 來更新state。官方的注釋里還寫道,如果在開發模式下啟用了熱更新,同樣需要這個函數來進行替換。
function observable() { const outerSubscribe = subscribe return { subscribe(observer) { if (typeof observer !== "object" || observer === null) { throw new TypeError("Expected the observer to be an object.") } function observeState() { if (observer.next) { observer.next(getState()) } } observeState() const unsubscribe = outerSubscribe(observeState) return { unsubscribe } }, [$$observable]() { return this } } }
observable:此方法返回一個 observable 對象,該對象擁有 subscribe 的方法來添加一個可觀測的對象。那么這個方法到底時干什么用的呢?注釋里有寫道 “For more information, see the observable proposal:https://github.com/tc39/propo...”,打開這個地址看到這個項目是將 可觀察類型引入到ECMAScript標準庫中,而redux 這里就是實現了 observable 的觀察對象。這里不是很清楚如何使用 ... 略過。
dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable }
最后使用 dispatch({ type: ActionTypes.INIT }) 生成一個初始化的state,這也就是為什么preloadedState不需要傳遞,就可以得到初始化的state了。因為redux內部在執行 createStore 這個方法的時候,自動執行了一次 disptach。最后將眾方法返回
③ combineReducers -- 將多個reducer 合并成一個函數
import ActionTypes from "./utils/actionTypes" import warning from "./utils/warning" import isPlainObject from "./utils/isPlainObject" // 函數1 function getUndefinedStateErrorMessage(key, action) { // ... } // 函數2 function getUnexpectedStateShapeWarningMessage( inputState, reducers, action, unexpectedKeyCache ) { // ... } // 函數3 function assertReducerShape(reducers) { // ... } // 函數4 export default function combineReducers(reducers) { // ... }
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.` ) }
getUndefinedStateErrorMessage: 方法比較簡單,返回一段提示信息,提示 reducer不能返回undefined
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" // 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." ) } // state 為非明確的對象的時候的提示 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("", "")}"` ) } // 當state當中的某個key值無法在reducers當中找到,并且還未被加入到unexpectedKeyCache // 那么就把這個key篩選出來 const unexpectedKeys = Object.keys(inputState).filter( key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key] ) // 將上一步驟篩選出來的key,存儲到unexpectedKeyCache里面 unexpectedKeys.forEach(key => { unexpectedKeyCache[key] = true }) // 如果是使用了 replaceReducer 替換了reducer,那么就不需要進行提示了,因為之前 // 的state 的數據可能和 新的reducer 不能保持一致 if (action && action.type === ActionTypes.REPLACE) return // 將state里面reducer不能操作的key打印出來 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: 在非生產環境對finalReducer進行的校驗,將state內異常的 key 拋出,進而提示開發者。
function assertReducerShape(reducers) { // 循環所有reducer, 并使用 ActionTypes.INIT 和 ActionTypes.PROBE_UNKNOWN_ACTION() // 兩個type類型的 action 校驗所有reducer 是否返回了符合規范的 state 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.` ) } if ( typeof reducer(undefined, { type: ActionTypes.PROBE_UNKNOWN_ACTION() }) === "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.` ) } }) }
assertReducerShape:用來斷言reducers返回的結果是不是符合要求的類型,如果不滿足判斷它會拋出一個error
可以看出在不使用 combineReducers 的時候,我們編寫的唯一的reducer是不用這些校驗的,如果我們在其中返回了 undefined 那么必然最終的 currentState 就變成了 undefined。
那么為什么 combineReducers 這個方法強制所有的 reduer 不能返回 undefined 呢?
找到了redux的中文文檔:http://cn.redux.js.org/docs/a...
里面提到:”combineReducers 函數設計的時候有點偏主觀,就是為了避免新手犯一些常見錯誤。也因些我們故意設定一些規則,但如果你自己手動編寫根 redcuer 時并不需要遵守這些規則“
這樣就很清楚了,原來多的這些校驗是為了更好的提示新手用戶,reducer的正確使用規范。
這里還有一個問題,使用 {type: ActionTypes.INIT} 來校驗 reducer 是否返回正確的state,ActionTypes.INIT 應該必然是 reducer 內部未知的 action 了。但是為什么下面還要用 { type: ActionTypes.PROBE_UNKNOWN_ACTION() } 在重復校驗一次嗎?難道說只是為了說明兩次校驗的出發目的不一樣?但是這樣是不是多余了
export default function combineReducers(reducers) { // 接收一個對象形式的 reducers 集合如 { reducer1: () => {}, reducer2: () => {}, } const reducerKeys = Object.keys(reducers) // 最終將要被執行的reducers 集合 const finalReducers = {} for (let i = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i] // 判斷當前如果是非生成環境 并且 reducers[key] 不存在時在控制臺打印警告信息 if (process.env.NODE_ENV !== "production") { if (typeof reducers[key] === "undefined") { warning(`No reducer provided for key "${key}"`) } } // 只有當 reducers[key] 為函數類型時 才添加到 finalReducers 當中 if (typeof reducers[key] === "function") { finalReducers[key] = reducers[key] } } // 獲取finalReducers 當中的 key const finalReducerKeys = Object.keys(finalReducers) // 用來存放state中不能被操作的 key let unexpectedKeyCache if (process.env.NODE_ENV !== "production") { // 為了優化性能只在非生產模式下進行校驗 unexpectedKeyCache = {} } // 用來校驗finalReducers中每一個reducer是否合乎規范 let shapeAssertionError try { assertReducerShape(finalReducers) } catch (e) { shapeAssertionError = e } return function combination(state = {}, action) { // 到這里開始執行 reducer if (shapeAssertionError) { // 如果reducer返回了undefined直接拋出錯誤 throw shapeAssertionError } // 非生產環境進行提示 if (process.env.NODE_ENV !== "production") { const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, unexpectedKeyCache ) if (warningMessage) { warning(warningMessage) } } // 判斷 state 有沒用被更改 let hasChanged = false // 重新生成的state 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") { // 如果reducer 返回了undefined 拋出錯誤 const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey // 如果state 內的某個key的數據已經被更改過 此處必然是 true hasChanged = hasChanged || nextStateForKey !== previousStateForKey } // 根據state是否改動過 返回對應的state return hasChanged ? nextState : state } }
combineReducers:這個方法就是將多個reducers循環執行后最終返回一個合并后的state,這個方法還是比較簡單的。
④-- bindActionCreators,提供了一種調用dispatch的其他方式,代碼本身比較簡單,這里就不在贅述了
function bindActionCreator(actionCreator, dispatch) { return function() { return dispatch(actionCreator.apply(this, arguments)) } } export 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 boundActionCreators = {} for (const key in actionCreators) { const actionCreator = actionCreators[key] if (typeof actionCreator === "function") { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators }
⑤ -- compose, 組合函數,將多個函數參數按照傳入的順序組合成一個函數,并返回。第二個參數作為第一個函數的參數,第三個參數作為第二個函數的參數,依次類推。返回的函數接收的參數,將作為compose 初始參數的最后一個函數的參數,源碼如下
export default function compose(...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } // 最終 compose 返回的是一個 (...args) => a(b(...args)) 這樣的函數 // 那么這個函數內部調用的順序也就清楚了,執行這個函數的時候,首先會執行 // var c = b(...args), 然后將c 作為參數傳遞給 a,如果多個函數的話依次類推, // 最先執行最里面的那個參數,由里向外 return funcs.reduce((a, b) => (...args) => a(b(...args))) }
問題1,先執行了b(...args)函數,不就直接開始從里面執行了嗎?demo1
var funcs2 = [ function a() { console.log("a") }, function b() { console.log("b") }, function c(arg) { console.log(arg) console.log("c") } ] var t = compose(funcs2) t("d") // d c b a
說好的剝洋蔥呢,怎么就成半個了?
其實compose這個方法只是提供了組合函數的功能,真正想要實現完整的從外到內這種完整的洋蔥模型,還需要對傳遞到compose參數的每個函數做處理,它的每個函數一定是返回了一個新的函數,這樣才能確保它不會被簡單的執行,思考下面的demo
var funcs = [ function a(b) { return () => { console.log("a") return b(); } }, function b(c) { return () => { console.log("b"); return c(); } }, function c(arg) { return () => { console.log("c"); console.log(arg) } } ] var f = compose(funcs)("d"); f() // a b c d
這就和文章最開始實現的 logger 和 thunk 的中間件很相似了。a 里執行 b , 然后 b 里執行 c,一個中間件的雛形已經出現了。
⑥ -- applyMiddleware,redux內唯一實現的enhancer,用來擴展dispatch方法,也是redux中最難理解的地方,一睹真容吧,為了便于對執行過程的理解,這里貼一下中間件redux-thunk源碼的簡化版
({ dispatch, getState }) => next => action => { if (typeof action === "function") { return action(dispatch, getState); } return next(action); }
import compose from "./compose" export default function applyMiddleware(...middlewares) { // 這里 applyMiddleware 返回的函數接收的第一個參數是 creteStore, // 這就是之前 createStore內直接使用 enhancer(createStore)(reducer, preloadedState) // 的原因了。 return createStore => (...args) => { // store 就是最原始的 store 對象,這里還未對store.dispatch 進行擴展 const store = createStore(...args) // 聲明一個dispatch的方法,注意這里使用的時let,表示這個dispatch將要會被改變 // 而改變過后的dispatch 就是加入了中間價增強版的 dispatch let dispatch = () => { throw new Error( "Dispatching while constructing your middleware is not allowed. " + "Other middleware would not be applied to this dispatch." ) } // middlewareAPI和字面意思一樣就是,中間件需要用到的原始的store對象的方法,, // 這里提供了兩個方法,一個getState, 一個是dispatch。等等這里好像有點不一樣的東西 // 為什么這里 要寫成 dispatch: (...args) => dispatch(...args),而不是直接 // dispatch: dispatch 先留下繼續向下看。 const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) } // 這里參考上面的redux-thunk的源碼 chain最終返回的結果就是 /* chain = [ next => action => { if (typeof action === "function") { return action(dispatch, getState); // 此處的dispatch 和 getState 即為 middlewareAPI的dispatch 和 getState } return next(action); } ] */ const chain = middlewares.map(middleware => middleware(middlewareAPI)) // 結合之前已經分析過的compose,compose(...chain)最后一個參數來接收 store.dispatch // 因為這里只傳遞了一個thunk中間件,所以,這里的 dispatch 就變成了 /* dispatch = (action) => { if (typeof action === "function") { return action(dispatch, getState); // 此處的dispatch 和 getState 依然為 middlewareAPI的dispatch 和 getState } // 由于next被賦值為store.dispatch, 此處實際為 return store.dispatch(action); } */ // 也因為這里dispatch 被重新賦值,導致middlewareAPI內dispatch屬性 // 里使用到的 dispatch 也變了,不再是拋出error錯誤的那個函數了。 // 這就是為什么一定要寫成 dispatch: (...args) => dispatch(...args) 的原因了 // 如果寫成 dispatch: dispatch, 相當于只是初始聲明了這個方法,后續dispatch的修改就與它無關了 dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } // 返回的dispatch 替換掉了 store 內部初始的 dispatch, dispatch被擴展了 } }
總結:
通過對源碼的翻閱,了解到了整個redux的執行流程和機制。通過createStore來初始化state,當需要使用異步action的時候,可以使用 applyMiddleware 將redux-thunk redux-saga 等中間件對store.dispatch 進行擴展。每次調用 dispatch 都會執行所有的reducer,reducer執行完畢后,會更新所有的訂閱事件。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/103300.html
摘要:循環還沒有結束,其中的某個對進行了添加或者刪除,都會影響到此次循環的進行,帶來不可預期的錯誤。 首先來一段 redux 結合 中間件 thunk、logger 的使用demo 了解一下應該如何使用 const redux = require(redux) const { createStore, combineReducers, bindActionCreators, ...
摘要:最近開始看源碼,并將源碼解讀放在了我的計劃中。將轉為數組同時去掉第一個元素之后便可以調用方法總結數組的擴展方法就解讀到這里了,相關源碼可以參考這部分。放個預告,下一篇會暫緩下,講下相關的東西,敬請期待。 Why underscore 最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。 閱讀一些著名框架類庫的源碼,就好...
摘要:直接來看例子一目了然,第一個參數是對象,第二個參數可以是一系列的值,也可以是數組數組中含,也可以是迭代函數,我們根據值,或者迭代函數來過濾中的鍵值對,返回新的對象副本。 Why underscore 最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。 閱讀一些著名框架類庫的源碼,就好像和一個個大師對話,你會學到很多。...
閱讀 2722·2021-11-22 13:54
閱讀 1063·2021-10-14 09:48
閱讀 2292·2021-09-08 09:35
閱讀 1550·2019-08-30 15:53
閱讀 1166·2019-08-30 13:14
閱讀 606·2019-08-30 13:09
閱讀 2521·2019-08-30 10:57
閱讀 3334·2019-08-29 13:18