已經快一年沒有碰過 React 全家桶了,最近換了個專案組要用到 React 技術棧,所以最近又複習了一下;撿起舊知識的同時又有了一些新的收穫,在這裡作文以記之。
在閱讀文章之前,最好已經知道如何使用 Redux(不是 React-Redux)。
一、準備環境
為了更好的解讀原始碼,我們可以把原始碼拷貝到本地,然後搭建一個開發環境。Redux 的使用不依賴於 React,所以你完全可以在一個極為簡單的 JavaScript 專案中使用它。這裡不再贅述開發環境的搭建過程,需要的同學可以直接拷貝我的程式碼到本地,然後安裝依賴,執行專案。
$ git clone https://github.com/zhongdeming428/redux && cd redux
$ npm i
$ npm run dev
複製程式碼
二、閱讀原始碼
(1)原始碼結構
忽略專案中的那些說明文件什麼的,只看 src 這個原始檔目錄,其結構如下:
src
├── applyMiddleware.js // 應用中介軟體的 API
├── bindActionCreators.js // 轉換 actionCreators 的 API
├── combineReducers.js // 組合轉換 reducer 的 API
├── compose.js // 工具函式,用於巢狀呼叫中介軟體
├── createStore.js // 入口函式,建立 store 的 API
├── index.js // redux 專案的入口檔案,用於統一暴露所有 API
├── test
│ └── index.js // 我所建立的用於除錯的指令碼
└── utils // 專門放工具函式的目錄
├── actionTypes.js // 定義了一些 redux 預留的 action type
├── isPlainObject.js // 用於判斷是否是純物件
└── warning.js // 用於丟擲合適的警告資訊
複製程式碼
可以看出來 redux 的原始碼結構簡單清晰明瞭,幾個主要的(也是僅有的) API 被儘可能的分散到了單個的檔案模組中,我們只需要挨個的看就行了。
(2)index.js
上一小節說到 index.js 是 redux 專案的入口檔案,用於暴露所有的 API,所以我們來看看程式碼:
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'
// 不同的 API 寫在不同的 js 檔案中,最後通過 index.js 統一匯出。
// 這個函式用於判斷當前程式碼是否已經被打包工具(比如 Webpack)壓縮過,如果被壓縮過的話,
// isCrushed 函式的名稱會被替換掉。如果被替換了函式名但是 process.env.NODE_ENV 又不等於 production
// 的時候,提醒使用者使用生產環境下的精簡程式碼。
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.'
)
}
// 匯出主要的 API。
export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose,
__DO_NOT_USE__ActionTypes
}
複製程式碼
我刪除了所有的英文註釋以減小篇幅,如果大家想看原來的註釋,可以去 redux 的專案檢視原始碼。
可以看到在程式的頭部引入了所有的 API 模組以及工具函式,然後在底部統一匯出了。這一部分比較簡單,主要是 isCrushed 函式有點意思。作者用這個函式來判斷程式碼是否被壓縮過(判斷函式名是否被替換掉了)。
這一部分也引用到了工具函式,由於這幾個函式比較簡單,所以可以先看看它們是幹嘛的。
(3)工具函式
除了 compose 函式以外,所有的工具函式都被放在了 utils 目錄下。
actionTypes.js
這個工具模組定義了幾種 redux 預留的 action type,包括 reducer 替換型別、reducer 初始化型別和隨機型別。下面上原始碼:
// 定義了一些 redux 保留的 action type。
// 隨機字串確保唯一性。
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
複製程式碼
可以看出就是返回了一個 ActionTypes 物件,裡面包含三種型別:INIT、REPLACE 和 PROBE_UNKNOW_ACTION。分別對應之前所說的幾種型別,為了防止和使用者自定義的 action type 相沖突,刻意在 type 裡面加入了隨機值。在後面的使用中,通過引入 ActionType 物件來進行對比。
isPlainObject.js
這個函式用於判斷傳入的物件是否是純物件,因為 redux 要求 action 和 state 是一個純物件,所以這個函式誕生了。
上原始碼:
/**
* 判斷一個引數是否是純物件,純物件的定義就是它的建構函式為 Object。
* 比如: { name: 'isPlainObject', type: 'funciton' }。
* 而 isPlainObject 這個函式就不是純物件,因為它的建構函式是 Function。
* @param {any} obj 要檢查的物件。
* @returns {boolean} 返回的檢查結果,true 代表是純物件。
*/
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
}
複製程式碼
warning.js
這個函式用於丟擲適當的警告,沒啥好說的。
/**
* Prints a warning in the console if it exists.
*
* @param {String} message The warning message.
* @returns {void}
*/
export default function warning(message) {
/* eslint-disable no-console */
if (typeof console !== 'undefined' && typeof console.error === 'function') {
console.error(message)
}
/* eslint-enable no-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.
throw new Error(message)
} catch (e) {} // eslint-disable-line no-empty
}
複製程式碼
compose.js
這個函式用於巢狀呼叫中介軟體(middleware),進行初始化。
/**
* 傳入一系列的單引數函式作為引數(funcs 陣列),返回一個新的函式,這個函式可以接受
* 多個引數,執行時會將 funcs 陣列中的函式從右至左進行呼叫。
* @param {...Function} funcs 一系列中介軟體。
* @returns {Function} 返回的結果函式。
* 從右至左呼叫,比如: compose(f, g, h) 將會返回一個新函式
* (...args) => f(g(h(...args))).
*/
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
// 通過 reduce 方法迭代。
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製程式碼
(4)createStore.js
看完了工具函式和入口函式,接下來就要正式步入主題了。我們使用 redux 的重要一步就是通過 createStore 方法建立 store。那麼接下來看看這個方法是怎麼建立 store 的,store 又是個什麼東西呢?
我們看原始碼:
import $$observable from 'symbol-observable'
// 後面會講。
import ActionTypes from './utils/actionTypes'
// 引入一些預定義的保留的 action type。
import isPlainObject from './utils/isPlainObject'
// 判斷一個物件是否是純物件。
// 使用 redux 最主要的 API,就是這個 createStore,它用於建立一個 redux store,為你提供狀態管理。
// 它接受三個引數(第二三個可選),第一個是 reducer,用於改變 redux store 的狀態;第二個是初始化的 store,
// 即最開始時候 store 的快照;第三個引數是由 applyMiddleware 函式返回的 enhancer 物件,使用中介軟體必須
// 提供的引數。
export default function createStore(reducer, preloadedState, enhancer) {
// 下面這一段基本可以不看,它們是對引數進行適配的。
/*************************************引數適配****************************************/
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
// 如果傳遞了多個 enhancer,丟擲錯誤。
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'
)
}
// 如果沒有傳遞預設的 state(preloadedState 為函式型別,enhancer 為未定義型別),那麼傳遞的
// preloadedState 即為 enhancer。
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
// 如果 enhancer 為不為空且非函式型別,報錯。
throw new Error('Expected the enhancer to be a function.')
}
// 使用 enhancer 對 createStore 進行處理,引入中介軟體。注意此處沒有再傳遞 enhancer 作為引數。實際上 enhancer 會對 createStore 進行處理,然後返回一個實際意義上的 createStore 用於建立 store 物件,參考 applyMiddleware.js。
return enhancer(createStore)(reducer, preloadedState)
}
// 如果 reducer 不是函式型別,報錯。
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
/*********************************************************************************/
// 在函式內部定義一系列區域性變數,用於儲存資料。
let currentReducer = reducer // 儲存當前的 reducer。
let currentState = preloadedState // 用於儲存當前的 store,即為 state。
let currentListeners = [] // 用於儲存通過 store.subscribe 註冊的當前的所有訂閱者。
let nextListeners = currentListeners // 新的 listeners 陣列,確保不直接修改 listeners。
let isDispatching = false // 當前狀態,防止 reducer 巢狀呼叫。
// 顧名思義,確保 nextListeners 可以被修改,當 nextListeners 與 currentListeners 指向同一個陣列的時候
// 讓 nextListeners 成為 currentListeners 的副本。防止修改 nextListeners 導致 currentListeners 發生變化。
// 一開始我也不是很明白為什麼會存在 nextListeners,因為後面 dispatch 函式中還是直接把 nextListeners 賦值給了 currentListeners。
// 直接使用 currentListeners 也是可以的。後來去 redux 的 repo 搜了搜,發現了一個 issue(https://github.com/reduxjs/redux/issues/2157) 講述了這個做法的理由。
// 提交這段程式碼的作者的解釋(https://github.com/reduxjs/redux/commit/c031c0a8d900e0e95a4915ecc0f96c6fe2d6e92b)是防止 Array.slice 的濫用,只有在必要的時候呼叫 Array.slice 方法來複制 listeners。
// 以前的做法是每次 dispatch 都要 slice 一次,導致了效能的降低吧。
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
// 返回 currentState,即 store 的快照。
function getState() {
// 防止在 reducer 中呼叫該方法,reducer 會接受 state 引數。
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
}
// store.subscribe 函式,訂閱 dispatch。
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
// 不允許在 reducer 中進行訂閱。
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
// 每次操作 nextListeners 之前先確保可以修改。
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
// 每次操作 nextListeners 之前先確保可以修改。
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
// store.dispatch 函式,用於觸發 reducer 修改 state。
function dispatch(action) {
if (!isPlainObject(action)) {
// action 必須是純物件。
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
// 每個 action 必須包含一個 type 屬性,指定修改的型別。
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
// reducer 內部不允許派發 action。
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
// 呼叫 reducer 之前,先將標誌位置一。
isDispatching = true
// 呼叫 reducer,返回的值即為最新的 state。
currentState = currentReducer(currentState, action)
} finally {
// 呼叫完之後將標誌位置 0,表示 dispatch 結束。
isDispatching = false
}
// dispatch 結束之後,執行所有訂閱者的函式。
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
// 返回當前所使用的 action,這一步是中介軟體巢狀使用的關鍵,很重要。
return action
}
// 一個比較新的 API,用於動態替換當前的 reducers。適用於按需載入,程式碼拆分等場景。
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
// 執行預設的 REPLACE 型別的 action。在 combineReducers 函式中有使用到這個型別。
currentReducer = nextReducer
dispatch({ type: ActionTypes.REPLACE })
}
// 這是為了適配 ECMA TC39 會議的一個有關 Observable 的提案(參考https://github.com/tc39/proposal-observable)所寫的一個函式。
// 作用是訂閱 store 的變化,適用於所有實現了 Observable 的類庫(主要是適配 RxJS)。
// 我找到了引入這個功能的那個 commit:https://github.com/reduxjs/redux/pull/1632。
function observable() {
// outerSubscribe 即為外部的 subscribe 函式。
const outerSubscribe = subscribe
// 返回一個純物件,包含 subscribe 方法。
return {
subscribe(observer) {
if (typeof observer !== 'object' || observer === null) {
throw new TypeError('Expected the observer to be an object.')
}
// 用於給 subscribe 註冊的函式,嚴格按照 Observable 的規範實現,observer 必須有一個 next 屬性。
function observeState() {
if (observer.next) {
observer.next(getState())
}
}
observeState()
const unsubscribe = outerSubscribe(observeState)
return { unsubscribe }
},
// $$observable 即為 Symbol.observable,也屬於 Observable 的規範,返回自身。
[$$observable]() {
return this
}
}
}
// 初始化時 dispatch 一個 INIT 型別的 action,校驗各種情況。
dispatch({ type: ActionTypes.INIT })
// 返回一個 store 物件。
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}
複製程式碼
不難發現,我們的 store 物件就是一個純 JavaScript 物件。包含幾個屬性 API,而我們的 state 就儲存在 createStore 這個方法內部,是一個區域性變數,只能通過 getState 方法訪問到。這裡實際上是對閉包的利用,所有我們操作的 state 都儲存在 getState 方法內部的一個變數裡面。直到我們的程式結束(或者說 store 被銷燬),createStore 方法才會被回收,裡面的變數才會被銷燬。
而 subscribe 方法就是對觀察者模式的利用(注意不是釋出訂閱模式,二者有區別,不要混淆),我們通過 subscribe 方法註冊我們的函式,我們的函式會給儲存到 createStore 方法的一個區域性變數當中,每次 dispatch 被呼叫之後,都會遍歷一遍 currentListeners,依次執行其中的方法,達到我們訂閱的要求。
(5)combineReducers.js
瞭解了 createStore 到底是怎麼一回事,我們再來看看 combineReducers 到底是怎麼建立 reducer 的。
我們寫 reducer 的時候,實際上是在寫一系列函式,然後整個到一個物件的屬性上,最後傳給 combineReducers 進行處理,處理之後就可以供 createStore 使用了。
例如:
// 建立我們的 reducers。
const _reducers = {
items(items = [], { type, payload }) {
if (type === 'ADD_ITEMS') items.push(payload);
return items;
},
isLoading(isLoading = false, { type, payload }) {
if (type === 'IS_LOADING') return true;
return false;
}
};
// 交給 combineReducers 處理,適配 createStore。
const reducers = combineReducers(_reducers);
// createStore 接受 reducers,建立我們需要的 store。
const store = createStore(reducers);
複製程式碼
那麼 combineReducers 對我們的 reducers 物件進行了哪些處理呢?
下面的程式碼比較長,希望大家能有耐心。
import ActionTypes from './utils/actionTypes'
import warning from './utils/warning'
import isPlainObject from './utils/isPlainObject'
/**
* 用於獲取錯誤資訊的工具函式,如果呼叫你所定義的某個 reducer 返回了 undefined,那麼就呼叫這個函式
* 丟擲合適的錯誤資訊。
*
* @param {String} key 你所定義的某個 reducer 的函式名,同時也是 state 的一個屬性名。
*
* @param {Object} action 呼叫 reducer 時所使用的 action。
*/
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 中的某個屬性沒有對應的 reducer,那麼返回報錯資訊。
* 對於 REPLACE 型別的 action type,則不進行校驗。
* @param {Object} inputState
* @param {Object} reducers
* @param {Object} action
* @param {Object} unexpectedKeyCache
*/
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'
// 如果 reducers 長度為 0,返回對應錯誤資訊。
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] + // {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1]
`". Expected argument to be an object with the following ` + // 返回的是 inputState 的型別。
`keys: "${reducerKeys.join('", "')}"`
)
}
// 獲取所有 State 有而 reducers 沒有的屬性,加入到 unexpectedKeysCache。
const unexpectedKeys = Object.keys(inputState).filter(
key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
)
// 加入到 unexpectedKeyCache。
unexpectedKeys.forEach(key => {
unexpectedKeyCache[key] = true
})
// 如果是 REPLACE 型別的 action type,不再校驗未知鍵,因為按需載入的 reducers 不需要校驗未知鍵,現在不存在的 reducers 可能下次就加上了。
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.`
)
}
}
/**
* 用於校驗所有 reducer 的合理性:傳入任意值都不能返回 undefined。
* @param {Object} reducers 你所定義的 reducers 物件。
*/
function assertReducerShape(reducers) {
Object.keys(reducers).forEach(key => {
const reducer = reducers[key]
// 獲取初始化時的 state。
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.`
)
}
// 如果初始化校驗通過了,有可能是你定義了 ActionTypes.INIT 的操作。這時候重新用隨機值校驗。
// 如果返回 undefined,說明使用者可能對 INIT type 做了對應處理,這是不允許的。
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.`
)
}
})
}
// 把你所定義的 reducers 物件轉化為一個龐大的彙總函式。
// 可以看出,combineReducers 接受一個 reducers 物件作為引數,
// 然後返回一個總的函式,作為最終的合法的 reducer,這個 reducer
// 接受 action 作為引數,根據 action 的型別遍歷呼叫所有的 reducer。
export default function combineReducers(reducers) {
// 獲取 reducers 所有的屬性名。
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
// 遍歷 reducers 的所有屬性,剔除所有不合法的 reducer。
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') {
// 將 reducers 中的所有 reducer 拷貝到新的 finalReducers 物件上。
finalReducers[key] = reducers[key]
}
}
// finalReducers 是一個純淨的經過過濾的 reducers 了,重新獲取所有屬性名。
const finalReducerKeys = Object.keys(finalReducers)
let unexpectedKeyCache
// unexpectedKeyCache 包含所有 state 中有但是 reducers 中沒有的屬性。
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
let shapeAssertionError
try {
// 校驗所有 reducer 的合理性,快取錯誤。
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
// 這就是返回的新的 reducer,一個純函式。每次 dispatch 一個 action,都要執行一遍 combination 函式,
// 進而把你所定義的所有 reducer 都執行一遍。
return function combination(state = {}, action) {
if (shapeAssertionError) {
// 如果有快取的錯誤,丟擲。
throw shapeAssertionError
}
if (process.env.NODE_ENV !== 'production') {
// 非生產環境下校驗 state 中的屬性是否都有對應的 reducer。
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
let hasChanged = false
// state 是否改變的標誌位。
const nextState = {}
// reducer 返回的新 state。
for (let i = 0; i < finalReducerKeys.length; i++) { // 遍歷所有的 reducer。
const key = finalReducerKeys[i] // 獲取 reducer 名稱。
const reducer = finalReducers[key] // 獲取 reducer。
const previousStateForKey = state[key] // 舊的 state 值。
const nextStateForKey = reducer(previousStateForKey, action) // 執行 reducer 返回的新的 state[key] 值。
if (typeof nextStateForKey === 'undefined') {
// 如果經過了那麼多校驗,你的 reducer 還是返回了 undefined,那麼就要丟擲錯誤資訊了。
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
// 把返回的新值新增到 nextState 物件上,這裡可以看出來,你所定義的 reducer 的名稱就是對應的 state 的屬性,所以 reducer 命名要規範!
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
// 檢驗 state 是否發生了變化。
}
// 根據標誌位返回對應的 state。
return hasChanged ? nextState : state
}
}
複製程式碼
(6)applyMiddleware.js
combineReducers 方法程式碼比較多,但是實際邏輯還是很簡單的,接下來這個函式程式碼不多,但是邏輯要稍微複雜一點,它就是應用中介軟體的 applyMiddleware 函式。這個函式的思想比較巧妙,值得學習。
import compose from './compose'
// 用於應用中介軟體的函式,可以同時傳遞多箇中介軟體。中介軟體的標準形式為:
// const middleware = store => next => action => { /*.....*/ return next(action); }
export default function applyMiddleware(...middlewares) {
// 返回一個函式,接受 createStore 作為引數。args 引數即為 reducer 和 preloadedState。
return createStore => (...args) => {
// 在函式內部呼叫 createStore 建立一個 store 物件,這裡不會傳遞 enhancer,因為 applyMiddleware 本身就是在建立一個 enhancer,然後給 createStore 呼叫。
// 這裡實際上是通過 applyMiddleware 把 store 的建立推遲了。為什麼要推遲呢?因為要利用 middleWares 做文章,先初始化中介軟體,重新定義 dispatch,然後再建立 store,這時候建立的 store 所包含的 dispatch 方法就區別於不傳遞 enhancer 時所建立的 dispatch 方法了,其中包含了中介軟體所定義的一些邏輯,這就是為什麼中介軟體可以干預 dispatch 的原因。
const store = createStore(...args)
// 這裡對 dispatch 進行了重新定義,不管傳入什麼引數,都會報錯,這樣做的目的是防止你的中介軟體在初始化的時候就
// 呼叫 dispatch。
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) // 注意最後 dispatch 的時候不會訪問上面報錯的那個 dispatch 函式了,因為那個函式被下面的 dispatch 覆蓋了。
}
// 對於每一個 middleware,都傳入 middlewareAPI 進行呼叫,這就是中介軟體的初始化。
// 初始化後的中介軟體返回一個新的函式,這個函式接受 store.dispatch 作為引數,返回一個替換後的 dispatch,作為新的
// store.dispatch。
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// compose 方法把所有中介軟體串聯起來呼叫。用最終結果替換 dispatch 函式,之後所使用的所有 store.dispatch 方法都已經是
// 替換了的,加入了新的邏輯。
dispatch = compose(...chain)(store.dispatch)
// 初始化中介軟體以後,把報錯的 dispatch 函式覆蓋掉。
/**
* middle 的標準形式:
* const middleware = ({ getState, dispatch }) => next => action => {
* // ....
* return next(action);
* }
* 這裡 next 是經過上一個 middleware 處理了的 dispatch 方法。
* next(action) 返回的仍然是一個 dispatch 方法。
*/
return {
...store,
dispatch // 全新的 dispatch。
}
}
}
複製程式碼
程式碼量真的很少,但是真的很巧妙,這裡有幾點很關鍵:
-
compose 方法利用
Array.prototype.reduce
實現中介軟體的巢狀呼叫,返回一個全新的函式,可以接受新的引數(上一個中介軟體處理過的 dispatch),最終返回一個全新的包含新邏輯的 dispatch 方法。你看 middleware 經過初始化後返回的函式的格式:next => action => { return next(action); } 複製程式碼
其中
next
可以看成dispatch
,這不就是接受一個 dispatch 作為引數,然後返回一個新的 dispatch 方法嗎?原因就是我們可以認為接受 action 作為引數,然後觸發 reducer 更改 state 的所有函式都是 dispatch 函式。 -
middleware 中介軟體經過初始化以後,返回一個新函式,它接受 dispatch 作為引數,然後返回一個新的 dispatch 又可以供下一個 middleware 呼叫,這就導致所有的 middleware 可以巢狀呼叫了!而且最終返回的結果也是一個 dispatch 函式。
最終得到的 dispatch 方法,是把原始的 store.dispatch 方法傳遞給最後一個 middleware,然後層層巢狀處理,最後經過第一個 middleware 處理過以後所返回的方法。所以我們在呼叫應用了中介軟體的 dispatch 函式時,從左至右的經過了 applyMiddleware 方法的所有引數(middleware)的處理。這有點像是包裹和拆包裹的過程。
-
為什麼 action 可以經過所有的中介軟體處理呢?我們再來看看中介軟體的基本結構:
({ dispatch, getState }) => next => action => { return next(action); } 複製程式碼
我們可以看到 action 進入函式以後,會經過 next 的處理,並且會返回結果。next 會返回什麼呢?因為第一個 next 的值就是 store.dispatch,所以看看 store.dispatch 的原始碼就知道了。
function dispatch(action) { // 省略了一系列操作的程式碼…… // 返回當前所使用的 action,這一步是中介軟體巢狀使用的關鍵。 return action } 複製程式碼
沒錯,store.dispatch 最終返回了 action,由於中介軟體巢狀呼叫,所以每個 next 都返回 action,然後又可以供下一個 next 使用,環環相扣,十分巧妙。
這部分描述的有點拗口,語言捉急但又不想畫圖,各位還是自己多想想好了。
(7)bindActionCreators.js
這個方法沒有太多好說的,主要作用是減少大家 dispatch reducer 所要寫的程式碼,比如你原來有一個 action:
const addItems = item => ({
type: 'ADD_ITEMS',
payload: item
});
複製程式碼
然後你要呼叫它的時候:
store.dispatch(addItems('item value'));
複製程式碼
如果你使用 bindActionCreators:
const _addItems = bindActionCreators(addItems, store.dispatch);
複製程式碼
當你要 dispatch reducer 的時候:
_addItems('item value');
複製程式碼
這樣就減少了你要寫的重複程式碼,另外你還可以把所有的 action 寫在一個物件裡面傳遞給 bindActionCreators,就像傳遞給 combineReducers 的物件那樣。
下面看看原始碼:
/**
* 該函式返回一個新的函式,呼叫新的函式會直接 dispatch ActionCreator 所返回的 action。
* 這個函式是 bindActionCreators 函式的基礎,在 bindActionCreators 函式中會把 actionCreators 拆分成一個一個
* 的 ActionCreator,然後呼叫 bindActionCreator 方法。
* @param {Function} actionCreator 一個返回 action 純物件的函式。
* @param {Function} dispatch store.dispatch 方法,用於觸發 reducer。
*/
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
// 接受一個 actionCreator(或者一個 actionCreators 物件)和一個 dispatch 函式作為引數,
// 然後返回一個函式或者一個物件,直接執行這個函式或物件中的函式可以讓你不必再呼叫 dispatch。
export default function bindActionCreators(actionCreators, dispatch) {
// 如果 actionCreators 是一個函式而非物件,那麼直接呼叫 bindActionCreators 方法進行轉換,此時返回
// 結果也是一個函式,執行這個函式會直接 dispatch 對應的 action。
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
// actionCreators 既不是函式也不是物件,或者為空時,丟擲錯誤。
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"?`
)
}
// 如果 actionCreators 是一個物件,那麼它的每一個屬性就應該是一個 actionCreator,遍歷每一個 actionCreator,
// 使用 bindActionCreator 進行轉換。
const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
// 把轉換結果繫結到 boundActionCreators 物件,最後會返回它。
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
複製程式碼
這部分挺簡單的,主要作用在於把 action creator 轉化為可以直接使用的函式。
三、中介軟體
看了原始碼以後,覺得中介軟體並沒有想象中的那麼晦澀難懂了。就是一個基本的格式,然後你在你的中介軟體裡面可以為所欲為,最後呼叫固定的方法,返回固定的內容就完事了。這就是為什麼大多數 redux middleware 的原始碼都很短小精悍的原因。
看看 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 可以被認為就是:
// 這就是典型的 middleware 格式。
({ dispatch, getState }) => next => action => {
// next 就是 dispatch 方法。註釋所在的函式就是返回的新的 dispatch。
// 先判斷一下 action 是不是一個函式。
if (typeof action === 'function') {
// 如果是函式,呼叫它,傳遞 dispatch,getState 和 多餘的引數作為 aciton 的引數。
return action(dispatch, getState, extraArgument);
}
// 如果 action 不是函式,直接 nextr呼叫 action,返回結果就完事兒了。
return next(action);
};
複製程式碼
怎麼樣,是不是很簡單?它乾的事就是判斷了一下 action 的型別,如果是函式就呼叫,不是函式就用 dispatch 來呼叫,很簡單。
但是它實現的功能很實用,允許我們傳遞函式作為 store.dispatch 的引數,這個函式的引數應該是固定的,必須符合上面程式碼的要求,接受 dispatch、getState作為引數,然後這個函式應該返回實際的 action。
我們也可以寫一個自己的中介軟體了:
({ dispatch, getState }) => next => action => {
return action.then ? action.then(next) : next(action);
}
複製程式碼
這個中介軟體允許我們傳遞一個 Promise 物件作為 action,然後會等 action 返回結果(一個真正的 action)之後,再進行 dispatch。
當然由於 action.then() 返回的不是實際上的 action(一個純物件),所以這個中介軟體可能沒法跟其他中介軟體一起使用,不然其他中介軟體接受不到 action 會出問題。這只是個示例,用於說明中介軟體沒那麼複雜,但是我們可以利用中介軟體做很多事情。
如果想要了解更加複雜的 redux 中介軟體,可以參考:
四、總結
- Redux 精妙小巧,主要利用了閉包和觀察者模式,然後運用了職責鏈、介面卡等模式構建了一個 store 王國。store 擁有自己的領土,要想獲取或改變 store 裡面的內容,必須通過 store 的各個函式來實現。
- Redux 相比於 Vuex 而言,程式碼量更小,定製化程度更低,這就導致易用性低於 Vuex,但是可定製性高於 Vuex。這也符合 Vue 和 React 的風格。
- Redux 原始碼比較好懂,讀懂原始碼更易於掌握 Redux 的使用方法,不要被嚇倒。
- Redux 中介軟體短小精悍,比較實用。如果從使用方法開始學中介軟體比較難懂的話,可以嘗試從原始碼學習中介軟體。
最後,時間緊迫,水平有限,難免存在紕漏或錯誤,請大家多多包涵、多多指教、共同進步。