摘要:然后循環調用中的更新函數,更新函數一般是我們的渲染函數,函數內部會調用來獲取數據,所以頁面會更新。
歡迎訪問個人網站:https://www.neroht.com/
前言前幾天寫了一篇react另一個狀態管理工具Unstated的源碼解析。
開啟了我的看源碼之路。想一想用了好長時間的redux,但從沒有深究過原理,遇到報錯更是懵逼,所以就啃了一遍它的源碼,寫了這篇文章,分享我對于它的理解。
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({store.getState()}, 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創建的,那么我們來看一下它內部的邏輯
createStorecreateStore總共接收三個參數: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梳理一下它的機制:
createStore接收上面提到的三個參數創建一個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 } }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幾個比較核心的概念就講解完了,不得不說寫的真簡潔,函數之間的依賴關系讓我一度十分懵逼,要理解它還是要用源碼來跑一遍例子,
一遍一遍地看。
總結一下redux就是創建一個store來管理所有狀態,觸發action來改變store。關于redux的使用場景是非常靈活的,可以結合各種庫去用,我用慣了react,用的時候還要配合react-redux。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/109671.html
摘要:然后循環調用中的更新函數,更新函數一般是我們的渲染函數,函數內部會調用來獲取數據,所以頁面會更新。前言 前幾天寫了一篇react另一個狀態管理工具Unstated的源碼解析。 開啟了我的看源碼之路。想一想用了好長時間的redux,但從沒有深究過原理,遇到報錯更是懵逼,所以就啃了一遍它的源碼,寫了這篇文章, 分享我對于它的理解。 API概覽 看一下redux源碼的index.js,看到了我們最...
摘要:歡迎來我的個人站點性能優化其他優化瀏覽器關鍵渲染路徑開啟性能優化之旅高性能滾動及頁面渲染優化理論寫法對壓縮率的影響唯快不破應用的個優化步驟進階鵝廠大神用直出實現網頁瞬開緩存網頁性能管理詳解寫給后端程序員的緩存原理介紹年底補課緩存機制優化動 歡迎來我的個人站點 性能優化 其他 優化瀏覽器關鍵渲染路徑 - 開啟性能優化之旅 高性能滾動 scroll 及頁面渲染優化 理論 | HTML寫法...
閱讀 3487·2023-04-25 22:45
閱讀 1291·2021-11-11 16:54
閱讀 2798·2019-08-30 15:44
閱讀 3196·2019-08-30 15:44
閱讀 1654·2019-08-30 13:55
閱讀 948·2019-08-29 18:45
閱讀 1203·2019-08-29 17:25
閱讀 1016·2019-08-29 12:59