欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 社会 > 【react】Redux的设计思想与工作原理

【react】Redux的设计思想与工作原理

2025/1/6 5:14:10 来源:https://blog.csdn.net/m0_64455070/article/details/144852433  浏览:    关键词:【react】Redux的设计思想与工作原理

Redux 的设计理念

Redux 的设计采用了 Facebook 提出的 Flux 数据处理理念

在 Flux 中通过建立一个公共集中数据仓库 Store 进行管理,整体分成四个部分即: View (视图层)、Action (动作)、Dispatcher (派发器)、Store (数据层)

如下图所示,当我们想要修改仓库的数据时,需要从 View 中触发 Action,由 Dispatcher 派发到 Store 修改数据,从而驱动视图更新

这种设计的好处在于其数据流向是单一的,数据的修改一定是会经过 Action、Dispatcher 等动作才能实现,方便预测、维护状态的流向。

当我们了解了 Flux 的设计理念后,便可以照葫芦画瓢了。

如下图所示,在 Redux 中同样需要维护一个公共数据仓库 Store, 而数据流向只能通过 View 触发 Action、 Reducer更新派发, Store 改变从而驱动视图更新

工作原理

当我们了解了 Redux 的设计理念后,趁热打铁炫一波 Redux 的工作原理,我们知道使用 Redux 进行状态管理的第一步就是需要先创建数据仓库 Store, 也就会需要调用 createStore 方法。那我们就先拿 createStore 开炫。

createStore

从 Redux 源码中我们不难看出,createStore 接收 reducer初始化state中间件三个参数,当执行 createStore 时会记录当前的 state 状态,并返回 store 对象,包含 dispatch、subscribe、getState 等属性。

其中

  • dispatch: 用来触发 Action
  • subscribe: 当 store 值的改变将触发 subscribe 的回调
  • getState: 用来获取当前的 state 状态。

getState 比较简单,直接返回当前的 state 状态,接下来我们将着重了解 dispatch 与 subscribe 的实现。

function createStore(reducer, preloadedState, enhancer) {let currentReducer = reducer // 记录当前的 reducerlet currentState = preloadedState // 记录当前的 statelet isDispatching = false // 是否正在进行 dispatchfunction getState() {return currentState // 通过 getState 获取当前的 state}// 触发 actionfunction dispatch(action: A) {}function subscribe(listener: () => void) {}// 初始化 statedispatch({ type: ActionTypes.INIT } as A)// 返回一个 sttoreconst store = {dispatch: dispatch as Dispatch<A>,subscribe,getState}return store
}

dispatch

在 Redux 中, 修改数据的唯一方式就是通过 dispatch,而 dispatch 接受一个 action 对象作为参数,执行 dispatch 方法,将生成新的 state,并触发监听事件。

function dispatch(action) {// 如果已经在触发中,则不允许再次出发 dispatch (禁止套娃)// 例如:在 reducer 中触发 dispatchif (isDispatching) {throw new Error('Reducers may not dispatch actions.')}try {// 上锁isDispatching = true// 调用 reducer,获取新的 statecurrentState = currentReducer(currentState, action)} finally {isDispatching = false}// 触发订阅事件const listeners = (currentListeners = nextListeners)listeners.forEach(listener => {listener()})return action}

subscribe

在 Redux 中, 可以通过 subscribe 方法来订阅 store 的变化, 一旦 store 发生了变化, 就会执行订阅的回调函数

可以看到 subscribe 方法接收一个回调函数作为参数, 执行 subscribe 方法将会返回一个 unsubscribe 函数, 用于取消订阅


function subscribe(listener: () => void) {if (isDispatching) {throw new Error()}let isSubscribed = true // 防止调用多次 unsubscribeensureCanMutateNextListeners() // 确保 nextListeners 是 currentListeners 的快照,而不是同一个引用const listenerId = listenerIdCounter++nextListeners.set(listenerId, listener) //nextListeners 添加订阅事件// 取消订阅事件return function unsubscribe() {if (!isSubscribed) {return}if (isDispatching) {throw new Error()}isSubscribed = falseensureCanMutateNextListeners(); // 如果某个订阅事件执行了 unsubscribe, nextListeners 创建了新的内存地址,而原先的listeners 依然保持不变 (dispatch 方法中的312 行)nextListeners.delete(listenerId)currentListeners = null}}

ensureCanMutateNextListeners 与 currentListeners 的作用

承接上文,在 subscribe 中不管是注册监听还是取消监听都会调用 ensureCanMutateNextListeners 的方法,那么这个方法是做什么的呢?

从函数的逻辑上不难得出答案:

ensureCanMutateNextListeners 确保 nextListeners 是 currentListeners 的快照,而不是同一个引用

function ensureCanMutateNextListeners() {if (nextListeners === currentListeners) { // currentListeners 用来确保循环的稳定性nextListeners = new Map()currentListeners.forEach((listener, key) => {nextListeners.set(key, listener)})}
}

在 dispatch 或者 subscribe 函数中,都是通过 nextListeners 触发监听,那为何还需要使用 currentListeners?

这里就不卖关子了,这里的 currentListeners 用于确保在 dispatch 中 listener 的数量不会发生变化, 确保当前循环的稳定性。

请看下面的例子👇

const a = store.subscribe(() => {/* a */
});
const b = store.subscribe(() => a());
const c = store.subscribe(() => {/*/ c */
});
store.dispatch(action);

上面的代码在 Redux 中是被允许的, 通过 subscribe 注册监听函数 a、b、c,此时 nextListeners 指向 [a, b, c]

当执行 dispatch 时, listener、currentListeners、nextListeners 将指向地址 [a, b, c];

// dispatch 触发监听事件的逻辑
// 触发订阅事件 
const listeners = (currentListeners = nextListeners)
listeners.forEach(listener => { listener() })

当执行到 b 监听函数时,将解绑 a 函数的监听事件,如果直接修改 nextListeners, 在循环中操作数组是非常危险的事情, 因此借助 ensureCanMutateNextListeners、currentListeners 为 nextListeners 开辟了新的内存地址,对 nextListeners 的操作将不影响 listener。

实现一个 mini react-redux

上文我们说到,一个组件如果想从 store 存取公用状态,需要进行四步操作:

  1. import引入store
  2. getState获取状态
  3. dispatch修改状态
  4. subscribe订阅更新

代码相对冗余,我们想要合并一些重复的操作,而 react-redux 就提供了一种合并操作的方案:react-redux提供 Providerconnect 两个API, Provider 将 store 放进 this.context 里,省去了 import 这一步, connect将 getState、dispatch 合并进了this.props,并自动订阅更新,简化了另外三步,下面我们来看一下如何实现这两个API:

Provider

Provider 组件比较简单,接收 store 并放进全局的 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

下面我们来思考一下如何实现 connect ,我们先回顾一下connect的使用方法

connect(mapStateToProps, mapDispatchToProps)(App)

connect 接收 mapStateToProps、mapDispatchToProps 两个函数,然后返回一个高阶函数, 最终将 mapStateToProps、mapDispatchToProps 函数的返回值通过 props 形式传递给 App 组件

我们直接放出connect的实现代码,并不复杂:

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) }// 根据 mapStateToProps 把 state 挂到 this.props 上 { ...(mapStateToProps(store.getState())) }// 根据mapDispatchToProps把dispatch(action)挂到this.props上{ ...(mapDispatchToProps(store.dispatch)) }/>)}return connectComponent;}
}

可以看出 connect 通过 useContext 实现和 store 的链接,将 state 作为第一个参数传给 mapStateToProps、将 dispatch 作为第一个参数传递给 mapDispatchToProps,最终将结果通过 props 形式传递给子组件。

其实 connect 这种设计,是装饰器模式的实现,所谓装饰器模式,简单地说就是对类的一个包装,动态地拓展类的功能。这里的 connect 以及 React 中的高阶组件(HoC)都是这一模式的实现。

对类的装饰常用于拓展类的功能,对类中函数的装饰常用于 AOP 切面

@decorator
class A {}// 等同于class A {}
A = decorator(A) || A;

装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。 如果一定要装饰函数,可以使用高阶函数

mini react-redux

通过上文,我们了解了 Provider 与 connect 的实现,我们可以写个 mini react-redux 来测试一下

1 创建如下目录结构

image.png

2 实现 createStore 函数 创建一个 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' }); // 初始化 statereturn {getState,dispatch,subscribe}
}

3 实现 reducer

createStore 函数接收一个 reducer 方法,reducer 常用来分发 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 实现 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) }// 根据 mapStateToProps 把 state 挂到 this.props 上       { ...(mapStateToProps(store.getState())) }// 根据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);

运行项目,点击增加按钮,如能正确计数,我们整个redux、react-redux的流程就走通了。

中间件

在大部分场景下, 我们需要自定义 dispatch 的行为, 在 Redux 中, 我们可以使用 中间件来拓展 dispatch 的功能

类似于 Express 或者 Koa, 在这些框架中,我们可以使用中间件来拓展 请求 和 响应 之间的功能

而 Redux 中间件的作用是在 action 发出之后, 到达 reducer 之前, 执行一系列的任务

image.png

在 Redux 中我们可以通过 applyMiddleware 生成一个强化器 enhancer 作为 createStore 的第二个参数传递。

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

正如它们的名称所示,每个中间件在调度操作时都会打印一个数字

import store from './store'  store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })  
// log: '1'  
// log: '2'  
// log: '3'

在这个例子中,当触发 dispatch 的内部执行顺序如下:

  • The print1 middleware (which we see as store.dispatch)
  • The print2 middleware
  • The print3 middleware
  • The original store.dispatch
  • The root reducer inside store

实现一个中间件

从上文得知, 我们了解了如何使用中间件, 接下来我们将实现一个中间件。

在 Redux 中,中间件其实是由三个嵌套函数组成

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)  }  }  
}

最外层函数 exampleMiddleware 将会被 applyMiddleware 调用,并传入 storeAPI 对象( 形如 {dispatch, getState} ),

const loggerMiddleware = storeAPI => next => action => {  console.log('dispatching', action)  let result = next(action) //调用下一个中间件或最终的dispatchconsole.log('next state', storeAPI.getState())  return result  
}

在这个中间件中:

  1. storeAPI 是传递给中间件的 store 对象,包含 dispatch 和 getState 方法。
  2. next 是指向下一个中间件的函数,如果没有更多中间件,则指向原始的 dispatch 方法。
  3. action 是被分发的 action 对象。

写完 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 结束');return result  
}const logger2 = storeAPI => next => action => {  console.log('logger2 开始');const result = next(action)  console.log('logger2 结束');return result  
}const logger3 = storeAPI => next => action => {  console.log('logger3 开始');const result = next(action)  console.log('logger3 结束');return result  
}
const middlewares = applyMiddleware(logger1, logger2, logger3);
const store = createStore(reducer, middlewares);
store.dispatch({ type: 'add' });

最终将打印

从打印的记过来看,如果之前有接触过 Express 或者 Koa 的同学,应该可以很快发现,这个是一个洋葱模型

示例2:

在创建store时,使用applyMiddleware方法将中间件应用到store上

import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers'; // 假设您有一个根 reducer
import loggerMiddleware from './loggerMiddleware'; 
// 导入您实现的 logger 中间件const store = createStore(rootReducer,applyMiddleware(loggerMiddleware)
);

applyMiddleware 的实现原理

从上可知,Redux 提供了一个 applyMiddleware 方法用于将中间件拓展到 dispatch 上

具体是如何拓展的呢?

从源码我们不难看出,最终是通过 compose 也就是利用 reduce 方法,将下一个的中间件函数作为参数,在上一个中间件的函数体内执行。

注意这里传入 compose 内的每一个函数都是一个双层嵌套函数。

applyMiddleware 是一个高阶函数,它接收一个或多个中间件作为参数,并返回一个函数。这个函数接收 createStore 的参数(reducer、初始 state 和可选的 store 增强器),并返回一个新的 createStore 函数。新的 createStore 函数创建的 store 有一个增强的 dispatch 方法,该方法能够依次通过所有中间件。

// applyMiddleware 源码
export default function applyMiddleware(...middlewares
) {// 返回一个接收 createStore为入参的函数return createStore => (reducer, preloadedState) => {// 创建 storeconst 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}}
}
  1. compose 函数用于将多个中间件函数组合成一个。它使用 reduce 方法将中间件函数串联起来,形成一个洋葱模型。
  2. applyMiddleware 函数接收中间件数组,并返回一个函数。这个函数接收 createStore 函数和它的参数(reducer、初始 state 和可选的增强器),并返回一个新的 createStore 函数。
  3. 新的 createStore 函数创建的 store 有一个增强的 dispatch 方法,该方法通过所有中间件。
### 中间件在大部分场景下,我们需要自定义 `dispatch` 的行为。
在 Redux 中,我们可以使用中间件来拓展 `dispatch` 的功能。中间件的作用是在 action 发出之后,到达 reducer 之前,执行一系列的任务。
类似于 Express 或 Koa,在这些框架中,我们可以使用中间件来拓展请求和响应之间的功能。#### 实现一个 Logger 中间件```typescript
const loggerMiddleware = storeAPI => next => action => {console.log('Dispatching:', action);let result = next(action); // 调用下一个中间件或最终的 dispatchconsole.log('Next State:', storeAPI.getState());return result;
};
function compose(...funcs) {if (funcs.length === 0) {// infer the argument type so it is usable in inference down the linereturn (arg:) => arg}if (funcs.length === 1) {return funcs[0]}return funcs.reduce((a, b) =>(...args) =>a(b(...args)))
}

模拟洋葱模型

洋葱模型形象地描述了Redux中间件的工作流程。

想象一下,每个中间件都像洋葱的一层皮,action就像一颗子弹,从外向内穿过每一层中间件,然后再由内向外穿出。

在每层中间件中,都可以对action进行处理或修改,然后再传递给下一层。

工作流程

1、action的传递

当一个action被dispatch时,它会首先进入最外层的中间件。

在这个中间件中,可以对action进行处理,然后调用next(action)将action传递给下一层中间件。2、逐层处理

action会依次穿过每一层中间件,每层中间件都可以对action进行处理。

处理完毕后,再调用next(action)将action传递给下一层。

3、到达reducer

当action穿过所有中间件后,最终会到达reducer。

Reducer根据action的类型和当前的状态来计算出一个新的状态。

4、逐层返回

在action被reducer处理后,结果会由内向外逐层返回给每一层中间件。

在每层中间件中,可以对返回的结果进行进一步的处理或修改。

5、最终返回

当action从最内层中间件返回时,整个中间件链的处理流程结束。

最终的结果会被返回给dispatch函数,完成整个action的处理流程。

// 定义三个中间件
const middleware1 = store => next => action => {console.log('中间件1之前');const result = next(action); // 调用下一个中间件或reducerconsole.log('中间件1之后');return result;
};const middleware2 = store => next => action => {console.log('中间件2之前');const result = next(action); // 调用下一个中间件或reducerconsole.log('中间件2之后');return result;
};const middleware3 = store => next => action => {console.log('中间件3之前');const result = next(action); // 调用reducer(因为没有下一个中间件了)console.log('中间件3之后');return result;
};// 应用中间件到Redux store
const store = createStore(reducer,applyMiddleware(middleware1, middleware2, middleware3)
);// 派发一个action
store.dispatch({ type: 'SOME_ACTION' });

输出结果

中间件1之前
中间件2之前
中间件3之前
// reducer的处理结果(假设有日志输出)
中间件3之后
中间件2之后
中间件1之后

拓展

Redux洋葱模型和Koa洋葱模型的区别

Redux洋葱模型

1、概念

Redux洋葱模型描述了中间件如何围绕action和reducer进行工作。

每个中间件都像洋葱的一层皮,action从外向内穿过每一层中间件,然后再由内向外穿出。

2、执行流程

当一个action被dispatch时,它会首先进入最外层的中间件。

中间件可以对action进行处理或修改,然后调用next(action)将action传递给下一层中间件。

最终,action会到达reducer进行处理,然后结果会由内向外逐层返回给每一层中间件。

3、应用场景

Redux中间件常用于异步操作、日志记录、错误报告等功能。

例如,redux-thunk、redux-saga等中间件允许在Redux中进行异步操作。

Koa洋葱模型

1、概念

Koa洋葱模型描述了中间件如何围绕HTTP请求和响应进行工作。

每个中间件都像洋葱的一层皮,请求从外向内穿过每一层中间件,响应则由内向外穿出。

2、执行流程

当一个HTTP请求到达时,它会首先进入最外层的中间件。

中间件可以对请求进行处理或修改,然后调用next()将控制权传递给下一个中间件。

一旦所有的中间件都通过next()调用完成入栈阶段的执行,控制流开始回溯,即从最内部的中间件开始向外部返回响应。

3、应用场景

Koa中间件常用于日志记录、错误处理、身份验证、数据解析等HTTP请求和响应相关的功能。

Koa的中间件机制允许开发者在请求和响应之间添加额外的逻辑,从而实现对HTTP请求和响应的灵活处理。

区别总结

1、应用场景不同

Redux洋葱模型主要应用于Redux状态管理库中的中间件处理,特别是与异步操作和日志记录等相关的功能。

Koa洋葱模型则主要应用于Koa Web框架中的中间件处理,特别是与HTTP请求和响应相关的功能。

2、中间件写法不同

Redux中间件通常是一个函数,它接收store作为参数,并返回一个函数,这个函数再接收next和action作为参数。

Koa中间件则是一个异步函数(在Koa 2.x中使用async/await),它接收ctx(上下文对象)和next作为参数。

3、执行流程细节不同

在Redux洋葱模型中,action在穿过所有中间件后到达reducer进行处理,然后结果再逐层返回给中间件。

在Koa洋葱模型中,请求在穿过所有中间件后控制权开始回溯,中间件可以在这个过程中处理响应或进行任何清理操作。

码字不易,大佬们点点赞

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com