簡單梳理Redux的原始碼與執行機制
前言
前幾天寫了一篇react另一個狀態管理工具Unstated的原始碼解析。
開啟了我的看原始碼之路。想一想用了好長時間的redux,但從沒有深究過原理,遇到報錯更是懵逼,所以就啃了一遍它的原始碼,寫了這篇文章,
分享我對於它的理解。
API概覽
看一下redux原始碼的index.js,看到了我們最常用的幾個API:
- createStore
- combineReducers
- bindActionCreators
- applyMiddleware
- compose
不著急分析,我們先看一下Redux的基本用法:
import react from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
const root = document.getElementById('root')
// reducer 純函式
const reducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
// 建立一個store
const store = createStore(reducer)
const render = () => ReactDOM.render(
<div >
<span>{store.getState()}</span>
<button onClick=={() => store.dispatch({ type: 'INCREMENT' })}>INCREMENT</button>
<button onClick=={() => store.dispatch({ type: 'DECREMENT' })}>DECREMENT</button>
</div>,
root
)
render()
// store訂閱一個更新函式 ,待dispatch之後,執行這個更新函式,獲取新的值
store.subscribe(render)
這裡實現的是一個點選按鈕加減數字的效果,點選觸發的行為,與展示在頁面上的數字變化,都是通過redux進行的。我們通過這個例子來分析一下redux是怎麼工作的:
- 使用reducer建立一個store,便於我們通過store來與redux溝通
- 頁面上通過store.getState()拿到了當前的數字,初始值為0(在reducer中)
- store.subscribe(render),訂閱更新頁面的函式,在reducer返回新的值時,呼叫。(實際subscribe會把函式推入listeners陣列,在之後迴圈呼叫)
- 點選按鈕,告訴redux,我是要增加還是減少(呼叫dispatch,傳入action)
- 呼叫dispatch之後,dispatch函式內部會呼叫我們定義的reducer,結合當前的state,和action,返回新的state
- 返回新的state之後,呼叫subscribe訂閱的更新函式,更新頁面
目前為止,我們所有的操作都是通過store進行的,而store是通過createStore建立的,那麼我們來看一下它內部的邏輯
createStore
createStore總共接收三個引數:reducer,preloadedState,enhancer,
- reducer:一個純函式,接收上一個(或初始的)state,和action,根據action 的type返回新的state
- preloadedState:一個初始化的state,可以設定store中的預設值,
- enhancer:增強器,用來擴充套件store的功能
暴露給我們幾個常用的API:
- dispatch:接收一個action, 是一個object{type:'a_action_type'}作為引數,之後其內部會呼叫reducer,根據這個action,和當前state,返回新的state。
- subscribe:訂閱一個更新頁面的函式,放進linsteners陣列,用於在reducer返回新的狀態的時候被呼叫,更新頁面。
- getState:獲取store中的狀態
我們先通過接收的引數和暴露出來的api梳理一下它的機制:
首先是接收上面提到的三個引數建立一個store,store是儲存應用所有狀態的地方。同時暴露出三個方法,UI可以通過store.getState()獲取到store中的資料,
store.subscribe(),作用是讓store訂閱一個更新UI的函式,將這個函式push到listeners陣列中,等待執行。
store.dispatch()是更新store中資料的唯一方法,dispatch被呼叫後,首先會呼叫reducer,根據當前的state和action返回新的狀態。然後迴圈呼叫listeners中的更新函式,
更新函式一般是我們UI的渲染函式,函式內部會呼叫store.getState()來獲取資料,所以頁面會更新。
看一下createStore函式的結構
createStore(reducer, preloadedState, enhancer) {
// 轉換引數
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
function getState() {
// 返回當前的state, 可以呼叫store.getState()獲取到store中的資料,
...
}
function subscribe(listener) {
// 訂閱一個更新函式(listener),實際上的訂閱操作就是把listener放入一個listeners陣列
// 然後再取消訂閱,將更新函式從listeners陣列內刪除
// 但是注意,這兩個操作都是在dispatch不執行時候進行的。因為dispatch執行時候會迴圈執行更新函式,要保證listeners陣列在這時候不能被改變
...
}
function dispatch(action) {
// 接收action,呼叫reducer根據action和當前的state,返回一個新的state
// 迴圈呼叫listeners陣列,執行更新函式,函式內部會通過store.getState()獲取state,此時的state為最新的state,完成頁面的更新
...
}
return {
dispatch,
subscribe,
getState,
}
}
結構就是這樣,但是是如何串聯起來的呢?下面來看一下完整的程式碼(刪除了一些)
createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
// 有了這一層判斷,我們就可以這樣傳:createStore(reducer, initialState, enhancer)
// 或者這樣: createStore(reducer, enhancer),其中enhancer還會是enhancer。
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
// enhancer的作用是擴充套件store,所以傳入createStore來改造,
// 再傳入reducer, preloadedState生成改造後的store,這一有一點遞迴呼叫的意思
return enhancer(createStore)(reducer, preloadedState)
}
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
let currentReducer = reducer // 當前的reducer,還會有新的reducer
let currentState = preloadedState // 當前的state
let currentListeners = [] // 儲存更新函式的陣列
let nextListeners = currentListeners // 下次dispatch將會觸發的更新函式陣列
let isDispatching = false //類似一把鎖,如果正在dispatch action,那麼就做一些限制
// 這個函式的作用是判斷nextListeners 和 currentListeners是否是同一個引用,是的話就拷貝一份,避免修改各自相互影響
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
function getState() {
// 正在執行reducer的時候,是不能獲取state的,要等到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
}
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
// 由於dispatch函式會在reducer執行完畢後迴圈執行listeners陣列內訂閱的更新函式,所以要保證這個時候的listeners陣列
// 不變,既不能新增(subscribe)更新函式也不能刪除(unsubscribe)更新函式
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
ensureCanMutateNextListeners()
// 將更新函式推入到listeners陣列,實現訂閱
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)
}
}
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?'
)
}
// 正在dispatch的話不能再次dispatch,也就是說不可以同時dispatch兩個action
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
// 獲取到當前的state
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
// 迴圈執行當前的linstener
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
// dispatch一個初始的action,作用是不命中你reducer中寫的任何關於action的判斷,直接返回初始的state
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
// observable replaceReducer和$$observable主要面向庫開發者,這裡先不做解析
// replaceReducer,
// [$$observable]:
}
}
combineReducers
combineReducers用於將多個reducer合併為一個總的reducer,所以可以猜出來,
它最終返回的一定是一個函式,並且形式就是一般的reducer的形式,接收state和action,
返回狀態:
function combine(state, action) {
......
return state
}
來看一下核心程式碼:
export default function combineReducers(reducers) {
// 獲取到所有reducer的名字,組成陣列
const reducerKeys = Object.keys(reducers)
// 這個finalReducers 是最終的有效的reducers
const finalReducers = {}
// 以reducer名為key,reducer處理函式為key,生成finalReducers物件,形式如下
/* {
* reducerName1: f,
* reducerName2: f
* }
*/
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
// assertReducerShape用來檢查這每個reducer有沒有預設返回的state,
// 我們在寫reducer時候,都是要在switch中加一個default的,來預設返回初始狀態
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
// 這個函式,就是上邊說的返回的最後的那個終極reducer,傳入createStore,
// 然後在dispatch中呼叫,也就是currentReducer
// 這個函式的核心是根據finalReducer中儲存的所有reducer資訊,迴圈,獲取到每個reducer對應的state,
// 並依據當前dispatch的action,一起傳入當前迴圈到的reducer,生成新的state,最終,將所有新生成的
// state作為值,各自的reducerName為鍵,生成最終的state,就是我們在reduxDevTool中看到的state樹,形式如下:
/* {
* reducerName1: {
* key: 'value'
* },
* reducerName2: {
* key: 'value'
* },
* }
*/
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
// 存放最終的所有的state
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
// 獲取每個reducer的名字
const key = finalReducerKeys[i]
// 獲取每個reducer
const reducer = finalReducers[key]
// 獲取每個reducer的舊狀態
const previousStateForKey = state[key]
// 呼叫該reducer,根據這個reducer的舊狀態,和當前action來生成新的state
const nextStateForKey = reducer(previousStateForKey, action)
// 以各自的reducerName為鍵,新生成的state作為值,生成最終的state object,
nextState[key] = nextStateForKey
// 判斷所有的state變化沒變化
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 變化了,返回新的state,否則,返回舊的state
return hasChanged ? nextState : state
}
}
資源搜尋網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.com
applyMiddleware
redux原本的dispatch方法只能接受一個物件作為action
使用者操作 -> dispatch(action) -> reducer(prevState, action) -> 新的state -> 介面
這麼直接乾脆的操作固然好,可以讓每一步的操作可追蹤,方便定位問題,但是帶來一個壞處,比如,頁面需要發請求獲取資料,並且把資料放到action裡面,
最終通過reducer的處理,放到store中。這時,如何做呢?
使用者操作 -> dispatch(action) -> middleware(action) -> 真正的action -> reducer(prevState, action) -> 新的state -> 介面
重點在於dispatch(action) -> middleware(action) 這個操作,這裡的action可以是一個函式,在函式內我們就可以進行很多操作,包括呼叫API,
然後在呼叫API成功後,再dispatch真正的action。想要這麼做,那就是需要擴充套件redux(改造dispatch方法),也就是使用增強器:enhancer:
const store = createStore(rootReducer,
applyMiddleware(thunk),
)
applyMiddleware(thunk)就相當於一個enhancer,它負責擴充套件redux,說白了就是擴充套件store的dispatch方法。
既然要改造store,那麼就得把store作為引數傳遞進這個enhancer中,再吐出一個改造好的store。吐出來的這個store的dispatch方法,是enhancer改造store的最終實現目標。
回顧一下createStore中的這部分:
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
// 把createStore傳遞進enhancer
return enhancer(createStore)(reducer, preloadedState)
}
看下上邊的程式碼,首先判斷enhancer,也就是createStore的第三個引數不為undefined且為函式的時候,那麼去執行這個enhancer。
我們看到enhancer(createStore),是把createStore傳入,進行改造,先不管這個函式返回啥,我們先看它執行完之後還需要的引數
(reducer, preloadedState), 是不是有點眼熟呢?回想一下createStore的呼叫方法,createStore(reducer, state)。
由此可知enhancer(createStore)返回的是一個新的createStore,而這個createStore是被改造過後的,它內部的dispatch方法已經不是原來的了。至此,達到了改造store的效果。
那到底是如何改造的呢? 先不著急,我們不妨先看一個現成的中介軟體redux-thunk。要了解redux中介軟體的機制,必須要理解中介軟體是怎麼執行的。
我們先來看用不用它有什麼區別:
一般情況下,dispatch的action是一個純物件
store.dispatch({
type:'EXPMALE_TYPE',
payload: {
name:'123',
}
})
使用了thunk之後,action可以是函式的形式
function loadData() {
return (dispatch, getState) => { // 函式之內會真正dispatch action
callApi('/url').then(res => {
dispatch({
type:'LOAD_SUCCESS',
data: res.data
})
})
}
}
store.dispatch(loadData()) //派發一個函式
一般情況下,dispatch一個函式會直接報錯的,因為createStore中的dispatch方法內部判斷了action的型別。redux-thunk幫我們做的事就是改造dispatch,讓它可以dispatch一個函式。
看一下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({ dispatch, getState })(next)(action)。
其中,thunk({ dispatch, getState})(next)這部分,看它執行時接收的引數是一個action,那麼它必然是一個dispatch方法,在此處相當於改造過後的dispatch,而這部分會在applyMiddleware中去呼叫,(下邊會講到)
然後從左往右看,{ dispatch, getState }是當前store的dispatch和getState方法,是最原始的,便於在經過中介軟體處理之後,可以拿到最原始的dispatch去派發真正的action。
next則是被當前中介軟體改造之前的dispatch。注意這個next,他與前邊的dispatch並不一樣,next是被thunk改造之前的dispatch,也就是說有可能是最原始的dispatch,也有可能是被其他中介軟體改造過的dispatch。
為了更好理解,還是翻譯成普通函式巢狀加註釋吧
function createThunkMiddleware(extraArgument) {
return function({ dispatch, getState }) { //真正的中介軟體函式,內部的改造dispatch的函式是精髓
return function(next) { //改造dispatch的函式,這裡的next是外部傳進來的dispatch,可能是被其他中介軟體處理過的,也可能是最原本的
return function(action) { //這個函式就是改造過後的dispatch函式
if (typeof action === 'function') {
// 如果action是函式,那麼執行它,並且將store的dispatch和getState傳入,便於我們dispatch的函式內部邏輯執行完之後dispatch真正的action,
// 如上邊示例的請求成功後,dispatch的部分
return action(dispatch, getState, extraArgument);
}
// 否則說明是個普通的action,直接dispatch
return next(action);
}
}
}
}
const thunk = createThunkMiddleware();
總結一下:說白了,redux-thunk的作用就是判斷action是不是一個函式,是就去執行它,不是就用那個可能被別的中介軟體改造過的,也可能是最原始的dispatch(next)去派發這個action。
那麼接下來看一下applyMiddleware的原始碼:
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 => {
// 假設我們只是用了redux-thunk,那麼此時的middleware就相當於thunk,可以往上看一下thunk返回的函式,
// 就是這個: function({ dispatch, getState }),就會明白了
return middleware(middlewareAPI)
})
// 這裡的compose函式的作用就是,將所有的中介軟體函式串聯起來,中介軟體1結束,作為引數傳入中介軟體2,被它處理,
// 以此類推最終返回的是被所有中介軟體處理完的函式,最開始接收store.dispatch為引數,層層改造後被賦值到新的dispatch變數中
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
先看最簡單的情況:假設我們只使用了一個middleware(redux-thunk),就可以暫時拋開compose,那麼這裡的邏輯就相當於
dispatch = thunk(middlewareAPI)(store.dispatch)
是不是有點熟悉? 在redux-thunk原始碼中我們分析過:
真正呼叫thunk的時候,thunk({ dispatch, getState })(next)(action)
其中,thunk({ dispatch, getState })(next)這部分,相當於改造過後的dispatch,而這部分會在applyMiddleware中去呼叫
所以,這裡就將store的dispatch方法改造完成了,最後用改造好的dispatch覆蓋原來store中的dispatch。
來總結一下,
- 中介軟體和redux的applyMiddleware的關係。中介軟體(middleware)會幫我們改造原來store的dispatch方法
- 而applyMiddleware會將改造好的dispatch方法應用到store上(相當於將原來的dispatch替換為改造好的dispatch)
理解中介軟體的原理是理解applyMiddleware機制的前提
另外說一下,關於redux-thunk的一個引數:extraArgument這個引數不是特別重要的,一般是傳入一個例項,然後在我們需要在真正dispatch的時候需要這個引數的時候可以獲取到,比如傳入一個axios 的Instance,那麼在請求時候就可以直接用這個instance去請求了
import axiosInstance from '../request'
const store = createStore(rootReducer, applyMiddleware(thunk.withExtraArgument(axiosInstance)))
function loadData() {
return (dispatch, getState, instance) => {
instance.get('/url').then(res => {
dispatch({
type:'LOAD_SUCCESS',
data: res.data
})
})
}
}
store.dispatch(loadData())
總結
到這裡,redux幾個比較核心的概念就講解完了,不得不說寫的真簡潔,函式之間的依賴關係讓我一度十分懵逼,要理解它還是要用原始碼來跑一遍例子,一遍一遍地看。