一文詳解React Redux設(shè)計思想與工作原理
設(shè)計思想
在開始了解之前,我們需要先了解 Redux 解決了什么問題?
Redux 解決了什么問題
在沒有 Redux 之前, 如果組件之間存在大量通信,甚至有些通信跨越多個組件,或者多個組件之間共享一套數(shù)據(jù),簡單的父子組件間傳值不能滿足我們的需求,自然而然地,我們需要有一個地方存取和操作這些公共狀態(tài)。而 redux 就為我們提供了一種管理公共狀態(tài)的方案,便于管理比較復(fù)雜的通信場景。
Redux 的設(shè)計理念
Redux 的設(shè)計采用了 Facebook 提出的 Flux 數(shù)據(jù)處理理念
在 Flux 中通過建立一個公共集中數(shù)據(jù)倉庫 Store 進(jìn)行管理,整體分成四個部分即: View (視圖層)、Action (動作)、Dispatcher (派發(fā)器)、Store (數(shù)據(jù)層)
如下圖所示,當(dāng)我們想要修改倉庫的數(shù)據(jù)時,需要從 View 中觸發(fā) Action,由 Dispatcher 派發(fā)到 Store 修改數(shù)據(jù),從而驅(qū)動視圖更新
這種設(shè)計的好處在于其數(shù)據(jù)流向是單一的,數(shù)據(jù)的修改一定是會經(jīng)過 Action、Dispatcher 等動作才能實現(xiàn),方便預(yù)測、維護狀態(tài)的流向。
當(dāng)我們了解了 Flux 的設(shè)計理念后,便可以照葫蘆畫瓢了。
如下圖所示,在 Redux 中同樣需要維護一個公共數(shù)據(jù)倉庫 Store, 而數(shù)據(jù)流向只能通過 View 觸發(fā) Action、 Reducer更新派發(fā), Store 改變從而驅(qū)動視圖更新
工作原理
當(dāng)我們了解了 Redux 的設(shè)計理念后,趁熱打鐵炫一波 Redux 的工作原理,我們知道使用 Redux 進(jìn)行狀態(tài)管理的第一步就是需要先創(chuàng)建數(shù)據(jù)倉庫 Store, 也就會需要調(diào)用 createStore 方法。那我們就先拿 createStore 開炫。
createStore
從 Redux 源碼中我們不難看出,createStore 接收 reducer、初始化state、中間件三個參數(shù),當(dāng)執(zhí)行 createStore 時會記錄當(dāng)前的 state 狀態(tài),并返回 store 對象,包含 dispatch、subscribe、getState 等屬性。
其中
- dispatch: 用來觸發(fā) Action
- subscribe: 當(dāng) store 值的改變將觸發(fā) subscribe 的回調(diào)
- getState: 用來獲取當(dāng)前的 state 狀態(tài)。
getState 比較簡單,直接返回當(dāng)前的 state 狀態(tài),接下來我們將著重了解 dispatch 與 subscribe 的實現(xiàn)。
function createStore(reducer, preloadedState, enhancer) { let currentReducer = reducer // 記錄當(dāng)前的 reducer let currentState = preloadedState // 記錄當(dāng)前的 state let isDispatching = false // 是否正在進(jìn)行 dispatch function getState() { return currentState // 通過 getState 獲取當(dāng)前的 state } // 觸發(fā) action function dispatch(action: A) {} function subscribe(listener: () => void) {} // 初始化 state dispatch({ type: ActionTypes.INIT } as A) // 返回一個 sttore const store = { dispatch: dispatch as Dispatch<A>, subscribe, getState } return store }
dispatch
在 Redux 中, 修改數(shù)據(jù)的唯一方式就是通過 dispatch,而 dispatch 接受一個 action 對象作為參數(shù),執(zhí)行 dispatch 方法,將生成新的 state,并觸發(fā)監(jiān)聽事件。
function dispatch(action) { // 如果已經(jīng)在觸發(fā)中,則不允許再次出發(fā) dispatch (禁止套娃) // 例如:在 reducer 中觸發(fā) dispatch if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } try { // 上鎖 isDispatching = true // 調(diào)用 reducer,獲取新的 state currentState = currentReducer(currentState, action) } finally { isDispatching = false } // 觸發(fā)訂閱事件 const listeners = (currentListeners = nextListeners) listeners.forEach(listener => { listener() }) return action }
subscribe
在 Redux 中, 可以通過 subscribe 方法來訂閱 store 的變化, 一旦 store 發(fā)生了變化, 就會執(zhí)行訂閱的回調(diào)函數(shù)
可以看到 subscribe 方法接收一個回調(diào)函數(shù)作為參數(shù), 執(zhí)行 subscribe 方法將會返回一個 unsubscribe 函數(shù), 用于取消訂閱
function subscribe(listener: () => void) { if (isDispatching) { throw new Error() } let isSubscribed = true // 防止調(diào)用多次 unsubscribe ensureCanMutateNextListeners() // 確保 nextListeners 是 currentListeners 的快照,而不是同一個引用 const listenerId = listenerIdCounter++ nextListeners.set(listenerId, listener) //nextListeners 添加訂閱事件 // 取消訂閱事件 return function unsubscribe() { if (!isSubscribed) { return } if (isDispatching) { throw new Error() } isSubscribed = false ensureCanMutateNextListeners(); // 如果某個訂閱事件執(zhí)行了 unsubscribe, nextListeners 創(chuàng)建了新的內(nèi)存地址,而原先的listeners 依然保持不變 (dispatch 方法中的312 行) nextListeners.delete(listenerId) currentListeners = null } }
ensureCanMutateNextListeners 與 currentListeners 的作用
承接上文,在 subscribe 中不管是注冊監(jiān)聽還是取消監(jiān)聽都會調(diào)用 ensureCanMutateNextListeners 的方法,那么這個方法是做什么的呢?
從函數(shù)的邏輯上不難得出答案:
ensureCanMutateNextListeners 確保 nextListeners 是 currentListeners 的快照,而不是同一個引用
function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { // currentListeners 用來確保循環(huán)的穩(wěn)定性 nextListeners = new Map() currentListeners.forEach((listener, key) => { nextListeners.set(key, listener) }) } }
在 dispatch 或者 subscribe 函數(shù)中,都是通過 nextListeners 觸發(fā)監(jiān)聽,那為何還需要使用 currentListeners?
這里就不賣關(guān)子了,這里的 currentListeners 用于確保在 dispatch 中 listener 的數(shù)量不會發(fā)生變化, 確保當(dāng)前循環(huán)的穩(wěn)定性。
請看下面的例子??
const a = store.subscribe(() => { /* a */ }); const b = store.subscribe(() => a()); const c = store.subscribe(() => { /*/ c */ }); store.dispatch(action);
上面的代碼在 Redux 中是被允許的, 通過 subscribe 注冊監(jiān)聽函數(shù) a、b、c,此時 nextListeners 指向 [a, b, c]
當(dāng)執(zhí)行 dispatch 時, listener、currentListeners、nextListeners 將指向地址 [a, b, c];
// dispatch 觸發(fā)監(jiān)聽事件的邏輯 // 觸發(fā)訂閱事件 const listeners = (currentListeners = nextListeners) listeners.forEach(listener => { listener() })
當(dāng)執(zhí)行到 b 監(jiān)聽函數(shù)時,將解綁 a 函數(shù)的監(jiān)聽事件,如果直接修改 nextListeners, 在循環(huán)中操作數(shù)組是非常危險的事情, 因此借助 ensureCanMutateNextListeners、currentListeners 為 nextListeners 開辟了新的內(nèi)存地址,對 nextListeners 的操作將不影響 listener。
實現(xiàn)一個 mini react-redux
上文我們說到,一個組件如果想從 store 存取公用狀態(tài),需要進(jìn)行四步操作:
- import引入store
- getState獲取狀態(tài)
- dispatch修改狀態(tài)
- subscribe訂閱更新
代碼相對冗余,我們想要合并一些重復(fù)的操作,而 react-redux 就提供了一種合并操作的方案:react-redux提供 Provider
和 connect
兩個API, Provider 將 store 放進(jìn) this.context 里,省去了 import 這一步, connect將 getState、dispatch 合并進(jìn)了this.props,并自動訂閱更新,簡化了另外三步,下面我們來看一下如何實現(xiàn)這兩個API:
Provider
Provider
組件比較簡單,接收 store 并放進(jìn)全局的 context
對象,使 store
可用于任何需要訪問 Redux store 的嵌套組件
import React, { createContext } from 'react'; let StoreContext; const Provider = (props) => { StoreContext = createContext(props.store); return <StoreContext.Provider value={props.store}>{ props.children }</StoreContext.Provider> }
connect
下面我們來思考一下如何實現(xiàn) connect
,我們先回顧一下connect的使用方法
connect(mapStateToProps, mapDispatchToProps)(App)
connect 接收 mapStateToProps、mapDispatchToProps 兩個函數(shù),然后返回一個高階函數(shù), 最終將 mapStateToProps、mapDispatchToProps 函數(shù)的返回值通過 props 形式傳遞給 App 組件
我們直接放出connect的實現(xiàn)代碼,并不復(fù)雜:
import React, { createContext, useContext, useEffect } from 'react'; export function connect(mapStateToProps, mapDispatchToProps) { return function (Component) { const connectComponent: React.FC = (props) => { const store = useContext(StoreContext); const [, updateState] = useState(); const forceUpdate = useCallback(() => updateState({}), []); const handleStoreChange = () => { // 強制刷新 forceUpdate(); } useEffect(() => { store.subscribe(handleStoreChange) }, []) return ( <Component // 傳入該組件的props,需要由connect這個高階組件原樣傳回原組件 { ...(props) } // 根據(jù) mapStateToProps 把 state 掛到 this.props 上 { ...(mapStateToProps(store.getState())) } // 根據(jù)mapDispatchToProps把dispatch(action)掛到this.props上 { ...(mapDispatchToProps(store.dispatch)) } /> ) } return connectComponent; } }
可以看出 connect 通過 useContext 實現(xiàn)和 store 的鏈接,將 state 作為第一個參數(shù)傳給 mapStateToProps、將 dispatch 作為第一個參數(shù)傳遞給 mapDispatchToProps,最終將結(jié)果通過 props 形式傳遞給子組件。
其實 connect 這種設(shè)計,是裝飾器模式的實現(xiàn),所謂裝飾器模式,簡單地說就是對類的一個包裝,動態(tài)地拓展類的功能。這里的 connect 以及 React 中的高階組件(HoC)都是這一模式的實現(xiàn)。
對類的裝飾常用于拓展類的功能,對類中函數(shù)的裝飾常用于 AOP 切面
@decorator class A {} // 等同于 class A {} A = decorator(A) || A;
裝飾器只能用于類和類的方法,不能用于函數(shù),因為存在函數(shù)提升。 如果一定要裝飾函數(shù),可以使用高階函數(shù)
mini react-redux
通過上文,我們了解了 Provider 與 connect 的實現(xiàn),我們可以寫個 mini react-redux 來測試一下
1 創(chuàng)建如下目錄結(jié)構(gòu)
2 實現(xiàn) createStore 函數(shù) 創(chuàng)建一個 createStore.ts 文件,createStore 最終將返回 store 對象,包含 getState、dispatch、subscribe
export const createStore = (reducer: Function) => { let currentState: undefined = undefined; const obervers: Array<Function> = []; function getState() { return currentState; } function dispatch(action: { type: string}) { currentState = reducer(currentState, action); obervers.forEach(fn => fn()); } function subscribe(fn: Function) { obervers.push(fn); } dispatch({ type: '@@REDUX/INIT' }); // 初始化 state return { getState, dispatch, subscribe } }
3 實現(xiàn) reducer
createStore 函數(shù)接收一個 reducer 方法,reducer 常用來分發(fā) action, 并返回新的 state
// reducer.ts const initialState = { count: 0 } export function reducer(state = initialState, action: { type: string}) { switch (action.type) { case 'add': return { ...state, count: state.count + 1 } case 'reduce': return { ...state, count: state.count - 1 } default: return initialState; } }
4 實現(xiàn) Provider 與 connect
/* eslint-disable react-hooks/rules-of-hooks */ //@ts-nocheck import React, { createContext, useContext, useEffect } from 'react'; let StoreContext; const Provider = (props) => { StoreContext = createContext(props.store); return <StoreContext.Provider value={props.store}>{ props.children }</StoreContext.Provider> } export default Provider; export function connect(mapStateToProps, mapDispatchToProps) { return function (Component) { const connectComponent: React.FC = (props) => { const store = useContext(StoreContext); const [, updateState] = React.useState(); const forceUpdate = React.useCallback(() => updateState({}), []); const handleStoreChange = () => { // 強制刷新 forceUpdate(); } useEffect(() => { store.subscribe(handleStoreChange) }, []) return ( <Component // 傳入該組件的props,需要由connect這個高階組件原樣傳回原組件 { ...(props) } // 根據(jù) mapStateToProps 把 state 掛到 this.props 上 { ...(mapStateToProps(store.getState())) } // 根據(jù)mapDispatchToProps把dispatch(action)掛到this.props上 { ...(mapDispatchToProps(store.dispatch)) } /> ) } return connectComponent; } }
5 修改 main.tsx
// main.tsx import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.tsx' import './index.css' import Provider from './react-redux/index.tsx'; import { createStore } from './react-redux/createStore.ts'; import { reducer } from './react-redux/reducer.ts'; ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <Provider store={createStore(reducer)}> <App /> </Provider> </React.StrictMode>, )
6 修改 App.tsx
// App.tsx import { useState } from 'react'; import { connect } from './react-redux'; const addAction = { type: 'add' } const mapStateToProps = (state: { count: number }) => { return { count: state.count } } const mapDispatchToProps = (dispatch: any) => { return { addCount: () => { dispatch(addAction) } } } interface Props { count: number; addCount: () => void; } function App(props: Props): JSX.Element { const { count, addCount } = props; return ( <div className="App"> { count } <button onClick={ () => addCount() }>增加</button> </div> ); } export default connect(mapStateToProps, mapDispatchToProps)(App);
運行項目,點擊增加按鈕,如能正確計數(shù),我們整個redux、react-redux的流程就走通了。
中間件
在大部分場景下, 我們需要自定義 dispatch 的行為, 在 Redux 中, 我們可以使用 中間件來拓展 dispatch 的功能
類似于 Express 或者 Koa, 在這些框架中,我們可以使用中間件來拓展 請求 和 響應(yīng) 之間的功能
而 Redux 中間件的作用是在 action 發(fā)出之后, 到達(dá) reducer 之前, 執(zhí)行一系列的任務(wù)
在 Redux 中我們可以通過 applyMiddleware 生成一個強化器 enhancer 作為 createStore 的第二個參數(shù)傳遞。
import { createStore, applyMiddleware } from 'redux' import rootReducer from './reducer' import { print1, print2, print3 } from './exampleAddons/middleware' const middlewareEnhancer = applyMiddleware(print1, print2, print3) // Pass enhancer as the second arg, since there's no preloadedState const store = createStore(rootReducer, middlewareEnhancer) export default store
正如它們的名稱所示,每個中間件在調(diào)度操作時都會打印一個數(shù)字
import store from './store' store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' }) // log: '1' // log: '2' // log: '3'
在這個例子中,當(dāng)觸發(fā) dispatch 的內(nèi)部執(zhí)行順序如下:
- The
print1
middleware (which we see asstore.dispatch
) - The
print2
middleware - The
print3
middleware - The original
store.dispatch
- The root reducer inside
store
實現(xiàn)一個中間件
從上文得知, 我們了解了如何使用中間件, 接下來我們將實現(xiàn)一個中間件。
在 Redux 中,中間件其實是由三個嵌套函數(shù)組成
function exampleMiddleware(storeAPI) { return function wrapDispatch(next) { return function handleAction(action) { // Do anything here: pass the action onwards with next(action), // or restart the pipeline with storeAPI.dispatch(action) // Can also use storeAPI.getState() here return next(action) } } }
最外層函數(shù) exampleMiddleware 將會被 applyMiddleware 調(diào)用,并傳入 storeAPI 對象( 形如 {dispatch, getState} ),
中間層函數(shù) wrapDispatch 接收一個 next 參數(shù),next 實際上就是中間管道的下一個中間件函數(shù),如果是最后一個 next,那么他的下一個中間件函數(shù)就是 dispatch
最內(nèi)層函數(shù) handleAction 接收一個 Action 對象
此時,我們知道了如何編寫一個中間件,接下來我們將實現(xiàn)一個 logger 中間件
const loggerMiddleware = storeAPI => next => action => { console.log('dispatching', action) let result = next(action) console.log('next state', storeAPI.getState()) return result }
寫完 logger 中間件后,我們嘗試在 Redux 中使用,如下
import { createStore, applyMiddleware } from "redux"; const initialState = { count: 0 } function reducer(state = initialState, action: { type: string}) { switch (action.type) { case 'add': return { ...state, count: state.count + 1 } case 'reduce': return { ...state, count: state.count - 1 } default: return initialState; } } const logger1 = storeAPI => next => action => { console.log('logger1 開始'); const result = next(action) console.log('logger1 結(jié)束'); return result } const logger2 = storeAPI => next => action => { console.log('logger2 開始'); const result = next(action) console.log('logger2 結(jié)束'); return result } const logger3 = storeAPI => next => action => { console.log('logger3 開始'); const result = next(action) console.log('logger3 結(jié)束'); return result } const middlewares = applyMiddleware(logger1, logger2, logger3); const store = createStore(reducer, middlewares); store.dispatch({ type: 'add' });
最終將打印
從打印的記過來看,如果之前有接觸過 Express 或者 Koa 的同學(xué),應(yīng)該可以很快發(fā)現(xiàn),這個是一個洋蔥模型
applyMiddleware 的實現(xiàn)原理
從上可知,Redux 提供了一個 applyMiddleware 方法用于將中間件拓展到 dispatch 上
具體是如何拓展的呢?
從源碼我們不難看出,最終是通過 compose 也就是利用 reduce 方法,將下一個的中間件函數(shù)作為參數(shù),在上一個中間件的函數(shù)體內(nèi)執(zhí)行。
注意這里傳入 compose 內(nèi)的每一個函數(shù)都是一個雙層嵌套函數(shù)。
// applyMiddleware 源碼 export default function applyMiddleware( ...middlewares ) { // 返回一個接收 createStore為入?yún)⒌暮瘮?shù) return createStore => (reducer, preloadedState) => { // 創(chuàng)建 store const store = createStore(reducer, preloadedState) let dispatch: Dispatch = () => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' ) } /** * middleware 形如: * ({dispatch, getState}) => next => action => { ... return next(action) } */ const middlewareAPI: MiddlewareAPI = { getState: store.getState, dispatch: (action, ...args) => dispatch(action, ...args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } }
function compose(...funcs) { if (funcs.length === 0) { // infer the argument type so it is usable in inference down the line return (arg:) => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce( (a, b) => (...args) => a(b(...args)) ) }
模擬洋蔥模型
承接上文,我們大概了解了什么是洋蔥模型,接下來我們將模擬一波洋蔥模型的實現(xiàn)。
const func1 = (fn) => () => { console.log('進(jìn)入func1', fn); const res = fn(); console.log('離開func1'); return res; } const func2 = (fn) => () => { console.log('進(jìn)入func2', fn); const res = fn(); console.log('離開func2'); return res; } const func3 = (fn) => () => { console.log('進(jìn)入func3', fn); const res = fn(); console.log('離開func3'); return res; } const composeB = (...fns) => { if (fns.length === 0) return arg => arg if (fns.length === 1) return fns[0] return fns.reduce((res, cur) => { return (...args) => res(cur(...args)) }); } // (...args) => func1((...args) => func2((...args) => func3(...args))) // 從左到右入棧 const dispatch = () => void 0; const c = composeB(func1, func2, func3)(dispatch); c();
總結(jié)
書寫至此,突然有一絲煽情,之前在下對于 redux 充滿了未知與恐懼,剛開始特別害怕學(xué)不懂,便遲遲不敢嘗試,不斷地擺爛,破罐子破摔??僧?dāng)靜下心來,接納自己的愚蠢,慢慢地一遍又一遍地讀每一行代碼與一些很 nice 的文章時,似乎恐懼是自己事前設(shè)定好的。而生活里也并不是只有成功 or 失敗,失敗也不應(yīng)該判定一個人的價值,所以不需要懼怕失敗。
以上就是一文詳解Redux設(shè)計思想與工作原理的詳細(xì)內(nèi)容,更多關(guān)于Redux設(shè)計思想與工作原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于useEffect的第二個參數(shù)解讀
這篇文章主要介紹了關(guān)于useEffect的第二個參數(shù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09