Redux 原始碼解讀
Redux 的原始碼非常的精煉,短短几百行程式碼,確提供了強大的功能。今天,我們就來一探究竟。
看原始碼最簡單的方式,就是從入口檔案來看,看它依賴哪些模組,然後在依次看這些模組的內容,最後也就對整個程式碼有個清晰的認識了。
所以我們就從入口檔案開始來看:
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' /* * This is a dummy function to check if the function name has been altered by minification. * If the function has been minified and NODE_ENV !== 'production', warn the user. */ function 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 looseenvify (https://github.com/zertosh/looseenvify) 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 } |
可以看到它依賴了下面這幾個模組:
- createStore
- combineReducers
- bindActionCreators
- applyMiddleware
- compose
- warning
其他沒什麼說的,就是把一些 API 暴露出去。那我們就先按照這個模組依賴順序,依次進行解讀。
createStore
首先是createStore
, 用來建立整個應用的 store .
它的依賴模組,都是些工具函式。
- isPlainObject
- $$observable
export default function createStore(reducer, preloadedState, enhancer) { if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined } if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) } if (typeof reducer !== 'function') { throw new Error('Expected the reducer to be a function.') } |
這裡邏輯很簡單:
第一個 if 語句的意思就是說,如果只傳入了兩個引數,且第二個引數 preloadedState 為函式,那麼就認為第二個引數為 enhancer .
第二個 if 語句確保 enhancer 是一個函式,並且當 enhancer 作為引數傳入的時候,返回 enhancer(createStore)(reucer, preloadedState) 作為 createStore 的返回,也就是我們要的 store.
第三個 if 語句確保 reducer 是一個函式。
接著往下看:
let currentReducer = reducer let currentState = preloadedState let currentListeners = [] let nextListeners = currentListeners let isDispatching = false function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } /** * Reads the state tree managed by the store. * * @returns {any} The current state tree of your application. */ function getState() { return currentState } |
這裡,把 preloadState
賦值給 currentState
,可以使應用直接重現某一個狀態,也可以用來做服務端渲染時直接由後臺計算出來作為應用的初始狀態。
ensureCanMutateNextListeners
這個函式在 nextListeners === currentListeners
成立時把 currentListeners
複製了一份賦值給了 nextListeners
. 用來做什麼還不太清楚,先放著。
然後定義了一個獲取當前 state 的方法。
subscribe
接下來是一個subscribe
方法。
/** * Adds a change listener. It will be called any time an action is dispatched, * and some part of the state tree may potentially have changed. You may then * call `getState()` to read the current state tree inside the callback. * * You may call `dispatch()` from a change listener, with the following * caveats: * * 1. The subscriptions are snapshotted just before every `dispatch()` call. * If you subscribe or unsubscribe while the listeners are being invoked, this * will not have any effect on the `dispatch()` that is currently in progress. * However, the next `dispatch()` call, whether nested or not, will use a more * recent snapshot of the subscription list. * * 2. The listener should not expect to see all state changes, as the state * might have been updated multiple times during a nested `dispatch()` before * the listener is called. It is, however, guaranteed that all subscribers * registered before the `dispatch()` started will be called with the latest * state by the time it exits. * * @param {Function} listener A callback to be invoked on every dispatch. * @returns {Function} A function to remove this change listener. */ function subscribe(listener) { if (typeof listener !== 'function') { throw new Error('Expected listener to be a function.') } let isSubscribed = true ensureCanMutateNextListeners() nextListeners.push(listener) return function unsubscribe() { if (!isSubscribed) { return } isSubscribed = false ensureCanMutateNextListeners() const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } } |
註釋已經說的非常明白了,註冊一個 listener
監聽函式,把他 push 到當前的監聽裡列表 nextListener
裡面,並返回一個 unsubscribe
方法用來登出當前這個監聽函式。
dispatch
function 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 } |
用來分發一個 action
來改變當前的 state . 也是唯一的改變 state 的方法。接受一個用來描述動作的 action
為引數,並且把這個 action
作為函式的返回值。
從程式碼前面的判斷可以看到,action 必須是一個字面量物件,並且必須包含一個 type
的屬性。
if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } |
從這裡可以看到,如果當前正處於上一個 action
的分發階段,那麼當前這個 action
有可能會分發失敗。
後面進行當前 state 的計算,並且按順序去觸發 nextListeners
裡面的監聽函式。
replaceReducer
/** * Replaces the reducer currently used by the store to calculate the state. * * You might need this if your app implements code splitting and you want to * load some of the reducers dynamically. You might also need this if you * implement a hot reloading mechanism for Redux. * * @param {Function} nextReducer The reducer for the store to use instead. * @returns {void} */ function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.') } currentReducer = nextReducer dispatch({ type: ActionTypes.INIT }) } |
替換掉當前的 reducer 並且分發一個用來初始化的內部 action.
export const ActionTypes = { INIT: '@@redux/INIT' } |
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/proposalobservable */ function observable() { const outerSubscribe = subscribe return { /** * The minimal observable subscription method. * @param {Object} observer Any object that can be used as an observer. * The observer object should have a `next` method. * @returns {subscription} An object with an `unsubscribe` method that can * be used to unsubscribe the observable from the store, and prevent further * emission of values from the observable. */ subscribe(observer) { if (typeof observer !== 'object') { 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 } } } |
用來把一個物件變成可 observe 的方法,一般情況下用不到。
最後
// When a store is created, an "INIT" action is dispatched so that every // reducer returns their initial state. This effectively populates // the initial state tree. dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } |
分發一個 INIT
的初始化 action ,用來讓所有的 reducer 來返回預設的初始化 state.
然後把上面的函式返回出來,作為通過 createStore
創建出來的 store 的 api.
combineReducers
這個模組用來合併多個 reducers 到一個 reducer,它的依賴模組:
- ActionTypes
- isPlainObject
- warning
我們依次來看看 combineReducers 裡面的內容。
getUndefinedStateErrorMessage
function getUndefinedStateErrorMessage(key, action) { const actionType = action && action.type const actionName = (actionType && `"${actionType.toString()}"`) || 'an action' return ( `Given action ${actionName}, 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.` ) } |
定義一個用來生成當 reducer 返回 undefined
時錯誤內容的函式,沒什麼好說的。
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([az|AZ]+)/)[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 (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.` ) } } |
從函式名 “獲取未期望 State 結構錯誤資訊” 可以看出這個函式用來生成當傳入的 inputState
組成結構錯誤時的錯誤資訊。
Reducer 必須有 key 值(這不廢話),inputState
必須是一個字面量物件。且inputState
的 key 都應該在 reducer 的自身屬性(OwnProperty, 非原型鏈上的)中,並且不能在傳入的 unexpectedKeyCache
中。
assertReducerShape
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.` ) } }) } |
用來保證傳入的 reducers
的結構正確,也就說說每個 reducer
都必須在收到 INIT action 後返回一個不為 undefined
的 initState
,並且這個 action
不能在 reducer
中專門去處理。這也是為什麼我們在 reducer 裡面一定要指定預設返回的 state 的原因.
combineReducers
/** * Turns an object whose values are different reducer functions, into a single * reducer function. It will call every child reducer, and gather their results * into a single state object, whose keys correspond to the keys of the passed * reducer functions. * * @param {Object} reducers An object whose values correspond to different * reducer functions that need to be combined into one. One handy way to obtain * it is to use ES6 `import * as reducers` syntax. The reducers may never return * undefined for any action. Instead, they should return their initial state * if the state passed to them was undefined, and the current state for any * unrecognized action. * * @returns {Function} A reducer function that invokes every reducer inside the * passed object, and builds a state object with the same shape. */ 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) let unexpectedKeyCache if (process.env.NODE_ENV !== 'production') { unexpectedKeyCache = {} } let shapeAssertionError try { assertReducerShape(finalReducers) } catch (e) { shapeAssertionError = e } 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 } } |
combineReducer
接收一個用來合併成一個 reducer
的物件,執行後返回一個函式,也即是我們的 rootReducer .
首先把傳入的 reducers
按 key
遍歷後賦值給 finalReducers
. 然後進行一堆錯誤判斷,最後返回一個函式 combination
. 也就是合併後的 reducer :
let hasChanged = false const nextState = {} // 遍歷 finalReducerKeys for (let i = 0; i < finalReducerKeys.length; i++) { // 拿到當前的 reducer key const key = finalReducerKeys[i] // 根據 reducer key 拿到具體的 reducer 函式 const reducer = finalReducers[key] // 獲取之前的 key 對應的 state const previousStateForKey = state[key] // 計算下一個當前 key 對應的 state const nextStateForKey = reducer(previousStateForKey, action) // 如果計算出來的 state 為 undefined 那麼報錯 if (typeof nextStateForKey === 'undefined') { const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } // 把當前 key 對應的 state 賦值到下一個全域性 state nextState[key] = nextStateForKey // 只要有一個 key 對應的 state 發生了變化,那麼就認為整個 state 發生了變化 hasChanged = hasChanged || nextStateForKey !== previousStateForKey } // 根據 state 是否發生變化,返回下一個 state 或者上一個 state return hasChanged ? nextState : state } |
bindActionCreators
這個函式非常簡單,是一個輔助函式。用來把 dispatch 繫結到一個 actionCreator 上,這樣當就可以通過直接呼叫繫結後的函式來分發一個 action ,而不需要 dispatch(actionCreator(…))
了。
applyMiddleware
這裡是重點,也是一般初學者難以理解的地方,我們仔細看看。
import compose from './compose' /** * Creates a store enhancer that applies middleware to the dispatch method * of the Redux store. This is handy for a variety of tasks, such as expressing * asynchronous actions in a concise manner, or logging every action payload. * * See `reduxthunk` package as an example of the Redux middleware. * * Because middleware is potentially asynchronous, this should be the first * store enhancer in the composition chain. * * Note that each middleware will be given the `dispatch` and `getState` functions * as named arguments. * * @param {...Function} middlewares The middleware chain to be applied. * @returns {Function} A store enhancer applying the middleware. */ export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => { const store = createStore(reducer, preloadedState, enhancer) let dispatch = store.dispatch let chain = [] const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } } |
程式碼量非常短,依賴了模組 compose
.
applyMiddleware
函式接受一系列中介軟體函式作為引數,返回了一個擁有 createStore
方法的閉包函式。這個函式,接收 reducer
,preloadedState
和 enhancer
為引數。
配合 createStore 函式來看:
export default function createStore(reducer, preloadedState, enhancer) { if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined } if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) } |
當我們這樣建立 store 的時候:
const store = createStore( reducer, applyMiddleware(...middleware) ) |
createStore 的第二個引數是個函式,所以就會走到
return enhancer(createStore)(reducer, preloadedState) |
也就是由 applyMiddleware(…middleware) 的結果接管了 createStore , 實際的 store 是在 applyMiddleware 裡面再次呼叫 createStore 建立的,此時傳入的 preloadedState, enhancer 都是 undefined.
// applyMiddleware const store = createStore(reducer, preloadedState, enhancer) |
回過頭來繼續往下看,
//applyMiddleware dispatch = compose(...chain)(store.dispatch) |
這裡需要先看一下 compose
這個模組,它的作用就是達到 compose(f, g, h) > (...args) => f(g(h(...args)))
這麼一個目的。
那麼這裡的 dispatch
就是在 store.dispatch
基礎上經過 middleware
加強封裝後的 dispatch
.
const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } // 把 middlewareAPI 傳入到每個中介軟體中 chain = middlewares.map(middleware => middleware(middlewareAPI)) |
這裡的 dispatch: (action) => dispatch(action)
,說明每個中介軟體中的 dispatch
都是獨立互不影響的,以免某個中介軟體中修改了 dispatch
的行為。然後給每個中介軟體都傳入 getState 和 dispatch 作為他們的引數。
return { ...store, dispatch } |
最後用加強後的 dispatch
覆蓋掉原有 store 中的 dispatch
.
整個中介軟體的程式碼看下來,可能比較抽象,我們結合一個例子來看一下:
errorMiddleware
export default ({dispatch, getState}) => next => action => { const {error, payload} = action if (error) { dispatch(showToast(payload.message || payload.toString())) } return next(action) } |
這是我們的一個錯誤處理中介軟體。它也是一個高階函式,首先接受 dispatch
, getState
為引數,返回一個接受 next
為引數的函式。dispatch
, getState
就是在上面程式碼裡通過 middlewareAPI 傳入了中介軟體中。
然後我們繼續看 errorMiddleware 執行後返回的接受 next
為引數的函式,而 next
其實就是下一個要執行的 middleware
.
然後我們需要了解一下中介軟體的執行順序,那麼為了更清楚的描述一個 action 在中介軟體中的傳播過程,我們假設有以下三個中介軟體:
const mid1 = () => next => action => { console.log('mid1 before') next(action) console.log('mid1 after') } const mid2 = () => next => action => { console.log('mid2 before') next(action) console.log('mid2 after') } const mid3 = () => next => action => { console.log('mid3 before') next(action) console.log('mid3 after') } |
執行 applyMiddleware( mid1, mid2, mid3 )
, 那麼經過下面程式碼後
dispatch = compose(...chain)(store.dispatch) |
就可以得到:
dispatch = (store.dispatch) => mid1(mid2(mid3(store.dispatch))) |
其中的 midx 都是已經執行了 middleware(middlewareAPI)
後返回的結果。所以 mid3
的 next
的值就是 store.dispatch
。而 mid2
的 next
則是 mid3(store.dispatch)
,以此類推,mid1
的 next 就是 mid2(mid3(store.dispatch))
, 這也就是在 middleware 呼叫 next
能夠讓 action
轉到下一個 middleware 的原因。
當我們分發一個 action 時,控制檯打印出來的順序是這樣的:
mid1 before mid2 before mid3 before mid3 after mid2 after mid1 after |
可以看到它的流程是這樣的:
- 執行 mid1 中 next 方法呼叫之前的程式碼
- 執行 mid2 中 next 方法呼叫之前的程式碼
- 執行 mid3 中 next 方法呼叫之前的程式碼
- 執行 dispatch 來分發 action
- 執行 mid3 中 next 方法呼叫之後的程式碼
- 執行 mid2 中 next 方法呼叫之後的程式碼
- 執行 mid1 中 next 方法呼叫之後的程式碼
看一張圖,會更明白一點:
其中紅色的路徑就是我們剛才描述的流程。可以看到其中還有一條黑色路徑,也就是如果我們直接在 mid2 中呼叫 dispatch 會怎麼樣?我們來改一下 mid2
const mid2 = ({ dispatch, getStore }) => next => action => { console.log('mid2 before') dispatch(action) console.log('mid2 after') } |
改成這樣,猜猜會怎樣?
答案是,會一直在 mid1 before 和 mid2 before 中死迴圈,因為呼叫的 dispatch
會讓這個 action
重新走一遍所有的中介軟體,也就是圖中的黑色路徑。那麼當我們需要在一箇中間件中呼叫 dispatch
的時候,是要對 action 做判斷的,只有滿足某個條件的時候才呼叫 dispatch
以免出現死迴圈。改造一下 mid2
const mid2 = ({ dispatch, getStore }) => next => action => { console.log('mid2 before') if(action.isApi) { dispatch({ isApi: false, ... }) } dispatch(action) console.log('mid2 after') } |
這樣,就只有在 action 滿足 isApi 條件的時候才會取分發一個不滿足 isApi 條件的 action ,這樣就不會死迴圈。一般在非同步分發 action 的時候會經常用這個方法。比如我們生產環境用來請求資料的 callAPIMiddleware :
export default ({dispatch, getState}) => { return next => action => { const { types, api, callType, meta, body, shouldCallAPI } = action const state = getState() const callTypeList = ['get', 'post'] if (!api) { return next(action) } if (!(types.start && types.success && types.failure)) { throw new Error('Expected types has start && success && failure keys.') } if (callTypeList.indexOf(callType) === 1) { throw new Error(`API callType Must be one of ${callTypeList}`) } const {start, success, failure} = types if (!shouldCallAPI(state)) { return false } dispatch({ type: start, payload: { ...body }, meta }) const mapCallTypeToFetch = { post: () => fetch(api, { method: 'post', // credentials 設定為每次請求都帶上 cookie credentials: 'include', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(bodyWithSource) }), get: () => { const toString = Object.keys(bodyWithSource).map(function (key, index) { return encodeURIComponent(key) + '=' + encodeURIComponent(bodyWithSource[key]) }).join('&') return fetch(`${api}?${toString}`, { method: 'get', credentials: 'include', headers: { 'Accept': 'application/json' } }) } } const fetching = mapCallTypeToFetch[callType]() ... 省略一堆業務邏輯 return fetching.then(res => { clearTimeout(loadingTimer) dispatch(hideLoading()) if (res.ok) { try { return res.json() } catch (err) { throw new Error(err) } } else { dispatch(showToast('請求出錯')) return Promise.reject(res.text()) } }) .then(res => resBehaviour(res)) .then(res => { dispatch({ type: success, meta, payload: { ...res.data } }) return Promise.resolve(res) }) .catch(err => { console.error(`介面請求出錯,${err}`) return Promise.reject(err) }) } |
關於中介軟體就說這麼多,大家應該也能理解了。
總結
總體上看, Redux 的原始碼非常短,但是各種實現都非常的精巧。
而且作者非常重視對開發者的體驗,註釋非常的詳細,整體上讀起來比較輕鬆。錯誤處理也非常詳細,可以幫助開發者更容易的定位錯誤。
最後,由於本人能力有限,文中如果有錯誤的地方,還請指出一起討論。
原文https://blog.kisnows.com/2018/11/20/redux-source-code-read/?utm_source=tuicool&utm_medium=referral