一文詳解React Redux設(shè)計(jì)思想與工作原理
設(shè)計(jì)思想
在開始了解之前,我們需要先了解 Redux 解決了什么問題?
Redux 解決了什么問題
在沒有 Redux 之前, 如果組件之間存在大量通信,甚至有些通信跨越多個(gè)組件,或者多個(gè)組件之間共享一套數(shù)據(jù),簡(jiǎn)單的父子組件間傳值不能滿足我們的需求,自然而然地,我們需要有一個(gè)地方存取和操作這些公共狀態(tài)。而 redux 就為我們提供了一種管理公共狀態(tài)的方案,便于管理比較復(fù)雜的通信場(chǎng)景。
Redux 的設(shè)計(jì)理念
Redux 的設(shè)計(jì)采用了 Facebook 提出的 Flux 數(shù)據(jù)處理理念
在 Flux 中通過建立一個(gè)公共集中數(shù)據(jù)倉(cāng)庫(kù) Store 進(jìn)行管理,整體分成四個(gè)部分即: View (視圖層)、Action (動(dòng)作)、Dispatcher (派發(fā)器)、Store (數(shù)據(jù)層)
如下圖所示,當(dāng)我們想要修改倉(cāng)庫(kù)的數(shù)據(jù)時(shí),需要從 View 中觸發(fā) Action,由 Dispatcher 派發(fā)到 Store 修改數(shù)據(jù),從而驅(qū)動(dòng)視圖更新

這種設(shè)計(jì)的好處在于其數(shù)據(jù)流向是單一的,數(shù)據(jù)的修改一定是會(huì)經(jīng)過 Action、Dispatcher 等動(dòng)作才能實(shí)現(xiàn),方便預(yù)測(cè)、維護(hù)狀態(tài)的流向。
當(dāng)我們了解了 Flux 的設(shè)計(jì)理念后,便可以照葫蘆畫瓢了。
如下圖所示,在 Redux 中同樣需要維護(hù)一個(gè)公共數(shù)據(jù)倉(cāng)庫(kù) Store, 而數(shù)據(jù)流向只能通過 View 觸發(fā) Action、 Reducer更新派發(fā), Store 改變從而驅(qū)動(dòng)視圖更新

工作原理
當(dāng)我們了解了 Redux 的設(shè)計(jì)理念后,趁熱打鐵炫一波 Redux 的工作原理,我們知道使用 Redux 進(jìn)行狀態(tài)管理的第一步就是需要先創(chuàng)建數(shù)據(jù)倉(cāng)庫(kù) Store, 也就會(huì)需要調(diào)用 createStore 方法。那我們就先拿 createStore 開炫。
createStore
從 Redux 源碼中我們不難看出,createStore 接收 reducer、初始化state、中間件三個(gè)參數(shù),當(dāng)執(zhí)行 createStore 時(shí)會(huì)記錄當(dāng)前的 state 狀態(tài),并返回 store 對(duì)象,包含 dispatch、subscribe、getState 等屬性。
其中
- dispatch: 用來觸發(fā) Action
- subscribe: 當(dāng) store 值的改變將觸發(fā) subscribe 的回調(diào)
- getState: 用來獲取當(dāng)前的 state 狀態(tài)。
getState 比較簡(jiǎn)單,直接返回當(dāng)前的 state 狀態(tài),接下來我們將著重了解 dispatch 與 subscribe 的實(shí)現(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)
// 返回一個(gè) sttore
const store = {
dispatch: dispatch as Dispatch<A>,
subscribe,
getState
}
return store
}dispatch
在 Redux 中, 修改數(shù)據(jù)的唯一方式就是通過 dispatch,而 dispatch 接受一個(gè) action 對(duì)象作為參數(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ā)生了變化, 就會(huì)執(zhí)行訂閱的回調(diào)函數(shù)
可以看到 subscribe 方法接收一個(gè)回調(diào)函數(shù)作為參數(shù), 執(zhí)行 subscribe 方法將會(huì)返回一個(gè) unsubscribe 函數(shù), 用于取消訂閱
function subscribe(listener: () => void) {
if (isDispatching) {
throw new Error()
}
let isSubscribed = true // 防止調(diào)用多次 unsubscribe
ensureCanMutateNextListeners() // 確保 nextListeners 是 currentListeners 的快照,而不是同一個(gè)引用
const listenerId = listenerIdCounter++
nextListeners.set(listenerId, listener) //nextListeners 添加訂閱事件
// 取消訂閱事件
return function unsubscribe() {
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error()
}
isSubscribed = false
ensureCanMutateNextListeners(); // 如果某個(gè)訂閱事件執(zhí)行了 unsubscribe, nextListeners 創(chuàng)建了新的內(nèi)存地址,而原先的listeners 依然保持不變 (dispatch 方法中的312 行)
nextListeners.delete(listenerId)
currentListeners = null
}
}ensureCanMutateNextListeners 與 currentListeners 的作用
承接上文,在 subscribe 中不管是注冊(cè)監(jiān)聽還是取消監(jiān)聽都會(huì)調(diào)用 ensureCanMutateNextListeners 的方法,那么這個(gè)方法是做什么的呢?
從函數(shù)的邏輯上不難得出答案:
ensureCanMutateNextListeners 確保 nextListeners 是 currentListeners 的快照,而不是同一個(gè)引用
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ù)量不會(huì)發(fā)生變化, 確保當(dāng)前循環(huán)的穩(wěn)定性。
請(qǐng)看下面的例子??
const a = store.subscribe(() => {
/* a */
});
const b = store.subscribe(() => a());
const c = store.subscribe(() => {
/*/ c */
});
store.dispatch(action);上面的代碼在 Redux 中是被允許的, 通過 subscribe 注冊(cè)監(jiān)聽函數(shù) a、b、c,此時(shí) nextListeners 指向 [a, b, c]
當(dāng)執(zhí)行 dispatch 時(shí), 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ù)時(shí),將解綁 a 函數(shù)的監(jiān)聽事件,如果直接修改 nextListeners, 在循環(huán)中操作數(shù)組是非常危險(xiǎn)的事情, 因此借助 ensureCanMutateNextListeners、currentListeners 為 nextListeners 開辟了新的內(nèi)存地址,對(duì) nextListeners 的操作將不影響 listener。

實(shí)現(xiàn)一個(gè) mini react-redux
上文我們說到,一個(gè)組件如果想從 store 存取公用狀態(tài),需要進(jìn)行四步操作:
- import引入store
- getState獲取狀態(tài)
- dispatch修改狀態(tài)
- subscribe訂閱更新
代碼相對(duì)冗余,我們想要合并一些重復(fù)的操作,而 react-redux 就提供了一種合并操作的方案:react-redux提供 Provider 和 connect 兩個(gè)API, Provider 將 store 放進(jìn) this.context 里,省去了 import 這一步, connect將 getState、dispatch 合并進(jìn)了this.props,并自動(dòng)訂閱更新,簡(jiǎn)化了另外三步,下面我們來看一下如何實(shí)現(xiàn)這兩個(gè)API:
Provider
Provider 組件比較簡(jiǎn)單,接收 store 并放進(jìn)全局的 context 對(duì)象,使 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
下面我們來思考一下如何實(shí)現(xiàn) connect ,我們先回顧一下connect的使用方法
connect(mapStateToProps, mapDispatchToProps)(App)
connect 接收 mapStateToProps、mapDispatchToProps 兩個(gè)函數(shù),然后返回一個(gè)高階函數(shù), 最終將 mapStateToProps、mapDispatchToProps 函數(shù)的返回值通過 props 形式傳遞給 App 組件
我們直接放出connect的實(shí)現(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 = () => {
// 強(qiáng)制刷新
forceUpdate();
}
useEffect(() => {
store.subscribe(handleStoreChange)
}, [])
return (
<Component
// 傳入該組件的props,需要由connect這個(gè)高階組件原樣傳回原組件
{ ...(props) }
// 根據(jù) mapStateToProps 把 state 掛到 this.props 上
{ ...(mapStateToProps(store.getState())) }
// 根據(jù)mapDispatchToProps把dispatch(action)掛到this.props上
{ ...(mapDispatchToProps(store.dispatch)) }
/>
)
}
return connectComponent;
}
}可以看出 connect 通過 useContext 實(shí)現(xiàn)和 store 的鏈接,將 state 作為第一個(gè)參數(shù)傳給 mapStateToProps、將 dispatch 作為第一個(gè)參數(shù)傳遞給 mapDispatchToProps,最終將結(jié)果通過 props 形式傳遞給子組件。
其實(shí) connect 這種設(shè)計(jì),是裝飾器模式的實(shí)現(xiàn),所謂裝飾器模式,簡(jiǎn)單地說就是對(duì)類的一個(gè)包裝,動(dòng)態(tài)地拓展類的功能。這里的 connect 以及 React 中的高階組件(HoC)都是這一模式的實(shí)現(xiàn)。
對(duì)類的裝飾常用于拓展類的功能,對(duì)類中函數(shù)的裝飾常用于 AOP 切面
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;裝飾器只能用于類和類的方法,不能用于函數(shù),因?yàn)榇嬖诤瘮?shù)提升。 如果一定要裝飾函數(shù),可以使用高階函數(shù)
mini react-redux
通過上文,我們了解了 Provider 與 connect 的實(shí)現(xiàn),我們可以寫個(gè) mini react-redux 來測(cè)試一下
1 創(chuàng)建如下目錄結(jié)構(gòu)

2 實(shí)現(xiàn) createStore 函數(shù) 創(chuàng)建一個(gè) createStore.ts 文件,createStore 最終將返回 store 對(duì)象,包含 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 實(shí)現(xiàn) reducer
createStore 函數(shù)接收一個(gè) 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 實(shí)現(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 = () => {
// 強(qiáng)制刷新
forceUpdate();
}
useEffect(() => {
store.subscribe(handleStoreChange)
}, [])
return (
<Component
// 傳入該組件的props,需要由connect這個(gè)高階組件原樣傳回原組件
{ ...(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);運(yùn)行項(xiàng)目,點(diǎn)擊增加按鈕,如能正確計(jì)數(shù),我們整個(gè)redux、react-redux的流程就走通了。

中間件
在大部分場(chǎng)景下, 我們需要自定義 dispatch 的行為, 在 Redux 中, 我們可以使用 中間件來拓展 dispatch 的功能
類似于 Express 或者 Koa, 在這些框架中,我們可以使用中間件來拓展 請(qǐng)求 和 響應(yīng) 之間的功能
而 Redux 中間件的作用是在 action 發(fā)出之后, 到達(dá) reducer 之前, 執(zhí)行一系列的任務(wù)

在 Redux 中我們可以通過 applyMiddleware 生成一個(gè)強(qiáng)化器 enhancer 作為 createStore 的第二個(gè)參數(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正如它們的名稱所示,每個(gè)中間件在調(diào)度操作時(shí)都會(huì)打印一個(gè)數(shù)字
import store from './store'
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: '1'
// log: '2'
// log: '3'在這個(gè)例子中,當(dāng)觸發(fā) dispatch 的內(nèi)部執(zhí)行順序如下:
- The
print1middleware (which we see asstore.dispatch) - The
print2middleware - The
print3middleware - The original
store.dispatch - The root reducer inside
store
實(shí)現(xiàn)一個(gè)中間件
從上文得知, 我們了解了如何使用中間件, 接下來我們將實(shí)現(xiàn)一個(gè)中間件。
在 Redux 中,中間件其實(shí)是由三個(gè)嵌套函數(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 將會(huì)被 applyMiddleware 調(diào)用,并傳入 storeAPI 對(duì)象( 形如 {dispatch, getState} ),
中間層函數(shù) wrapDispatch 接收一個(gè) next 參數(shù),next 實(shí)際上就是中間管道的下一個(gè)中間件函數(shù),如果是最后一個(gè) next,那么他的下一個(gè)中間件函數(shù)就是 dispatch
最內(nèi)層函數(shù) handleAction 接收一個(gè) Action 對(duì)象
此時(shí),我們知道了如何編寫一個(gè)中間件,接下來我們將實(shí)現(xiàn)一個(gè) 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),這個(gè)是一個(gè)洋蔥模型

applyMiddleware 的實(shí)現(xiàn)原理
從上可知,Redux 提供了一個(gè) applyMiddleware 方法用于將中間件拓展到 dispatch 上
具體是如何拓展的呢?
從源碼我們不難看出,最終是通過 compose 也就是利用 reduce 方法,將下一個(gè)的中間件函數(shù)作為參數(shù),在上一個(gè)中間件的函數(shù)體內(nèi)執(zhí)行。
注意這里傳入 compose 內(nèi)的每一個(gè)函數(shù)都是一個(gè)雙層嵌套函數(shù)。
// applyMiddleware 源碼
export default function applyMiddleware(
...middlewares
) {
// 返回一個(gè)接收 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))
)
}模擬洋蔥模型
承接上文,我們大概了解了什么是洋蔥模型,接下來我們將模擬一波洋蔥模型的實(shí)現(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é)
書寫至此,突然有一絲煽情,之前在下對(duì)于 redux 充滿了未知與恐懼,剛開始特別害怕學(xué)不懂,便遲遲不敢嘗試,不斷地?cái)[爛,破罐子破摔??僧?dāng)靜下心來,接納自己的愚蠢,慢慢地一遍又一遍地讀每一行代碼與一些很 nice 的文章時(shí),似乎恐懼是自己事前設(shè)定好的。而生活里也并不是只有成功 or 失敗,失敗也不應(yīng)該判定一個(gè)人的價(jià)值,所以不需要懼怕失敗。
以上就是一文詳解Redux設(shè)計(jì)思想與工作原理的詳細(xì)內(nèi)容,更多關(guān)于Redux設(shè)計(jì)思想與工作原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React?Flux與Redux設(shè)計(jì)及使用原理
這篇文章主要介紹了React?Flux與Redux設(shè)計(jì)及使用,Redux最主要是用作應(yīng)用狀態(tài)的管理。簡(jiǎn)言之,Redux用一個(gè)單獨(dú)的常量狀態(tài)樹(state對(duì)象)保存這一整個(gè)應(yīng)用的狀態(tài),這個(gè)對(duì)象不能直接被改變2023-03-03
詳解如何在React單頁(yè)面應(yīng)用中捕獲錯(cuò)誤
在當(dāng)前的Web開發(fā)中,使用React構(gòu)建單頁(yè)面應(yīng)用(SPA)已經(jīng)成為一種常見的做法,然而,當(dāng)應(yīng)用程序遇到錯(cuò)誤時(shí),有可能會(huì)導(dǎo)致整個(gè)頁(yè)面崩潰,給用戶帶來不好的體驗(yàn),本文將介紹如何在React單頁(yè)面應(yīng)用中捕獲錯(cuò)誤,以防止整個(gè)頁(yè)面的崩潰,需要的朋友可以參考下2023-09-09
關(guān)于useEffect的第二個(gè)參數(shù)解讀
這篇文章主要介紹了關(guān)于useEffect的第二個(gè)參數(shù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09

