一文详解React Redux设计思想与工作原理

 更新时间:2023年09月24日 17:26:14   作者:左耳咚  
最近看项目中使用了 Redux, 便尝试了解一波 Redux 的设计思想与工作原理,所以本文详细的给大家介绍了Redux设计思想与工作原理,需要的朋友可以参考下

设计思想

在开始了解之前,我们需要先了解 Redux 解决了什么问题?

Redux 解决了什么问题

在没有 Redux 之前, 如果组件之间存在大量通信,甚至有些通信跨越多个组件,或者多个组件之间共享一套数据,简单的父子组件间传值不能满足我们的需求,自然而然地,我们需要有一个地方存取和操作这些公共状态。而 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 // 记录当前的 reducer
   let currentState = preloadedState // 记录当前的 state
   let isDispatching = false // 是否正在进行 dispatch
   function getState() {
      return currentState // 通过 getState 获取当前的 state
   }
   // 触发 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 中, 修改数据的唯一方式就是通过 dispatch,而 dispatch 接受一个 action 对象作为参数,执行 dispatch 方法,将生成新的 state,并触发监听事件。

function dispatch(action) {
    // 如果已经在触发中,则不允许再次出发 dispatch (禁止套娃)
    // 例如:在 reducer 中触发 dispatch
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }
    try {
      // 上锁
      isDispatching = true
      // 调用 reducer,获取新的 state
      currentState = 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 // 防止调用多次 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(); // 如果某个订阅事件执行了 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 存取公用状态,需要进行四步操作:

  • import引入store
  • getState获取状态
  • dispatch修改状态
  • 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 创建如下目录结构

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' }); // 初始化 state
    return {
        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 之前, 执行一系列的任务

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

中间层函数 wrapDispatch 接收一个 next 参数,next 实际上就是中间管道的下一个中间件函数,如果是最后一个 next,那么他的下一个中间件函数就是 dispatch

最内层函数 handleAction 接收一个 Action 对象

此时,我们知道了如何编写一个中间件,接下来我们将实现一个 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 结束');
  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 的同学,应该可以很快发现,这个是一个洋葱模型

applyMiddleware 的实现原理

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

具体是如何拓展的呢?

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

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

// applyMiddleware 源码
export default function applyMiddleware(
  ...middlewares
) {
  // 返回一个接收 createStore为入参的函数
  return createStore => (reducer, preloadedState) => {
    // 创建 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))
  )
}

模拟洋葱模型

承接上文,我们大概了解了什么是洋葱模型,接下来我们将模拟一波洋葱模型的实现。

const func1 = (fn) => () => {
    console.log('进入func1', fn);
    const res = fn();
    console.log('离开func1');
    return res;
}
const func2 = (fn) => () => {
    console.log('进入func2', fn);
    const res = fn();
    console.log('离开func2');
    return res;
}
const func3 = (fn) => () => {
    console.log('进入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();

总结

书写至此,突然有一丝煽情,之前在下对于 redux 充满了未知与恐惧,刚开始特别害怕学不懂,便迟迟不敢尝试,不断地摆烂,破罐子破摔。可当静下心来,接纳自己的愚蠢,慢慢地一遍又一遍地读每一行代码与一些很 nice 的文章时,似乎恐惧是自己事前设定好的。而生活里也并不是只有成功 or 失败,失败也不应该判定一个人的价值,所以不需要惧怕失败。

以上就是一文详解Redux设计思想与工作原理的详细内容,更多关于Redux设计思想与工作原理的资料请关注脚本之家其它相关文章!

相关文章

  • React父子组件间的通信是怎样进行的

    React父子组件间的通信是怎样进行的

    这篇文章主要介绍了React中父子组件通信详解,在父组件中,为子组件添加属性数据,即可实现父组件向子组件通信,文章通过围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2023-03-03
  • react-router 重新加回跳转拦截功能详解

    react-router 重新加回跳转拦截功能详解

    这篇文章主要为大家介绍了react-router 重新加回跳转拦截功能详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02
  • 详解React-Todos入门例子

    详解React-Todos入门例子

    本篇文章主要介绍了React-Todos入门例子,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-11-11
  • React实现文件分片上传和下载的方法详解

    React实现文件分片上传和下载的方法详解

    在当今的前端开发中,处理文件流操作已经成为一个常见的需求,无论是上传、下载、读取、展示还是其他的文件处理操作,都需要高效且可靠地处理二进制数据,本文将深入探讨如何使用 React 实现文件分片上传和下载,并介绍相关的基本概念和技术,需要的朋友可以参考下
    2023-08-08
  • React如何通过@craco/craco代理接口

    React如何通过@craco/craco代理接口

    这篇文章主要介绍了React如何通过@craco/craco代理接口问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-10-10
  • 浅谈React前后端同构防止重复渲染

    浅谈React前后端同构防止重复渲染

    这篇文章主要介绍了浅谈React前后端同构防止重复渲染,首先解释React前后端同构、React首屏渲染的概念。然后通过这2个概念解决服务端渲染完成后浏览器端重复渲染的问题。有兴趣的可以了解一下
    2018-01-01
  • 从零搭建react+ts组件库(封装antd)的详细过程

    从零搭建react+ts组件库(封装antd)的详细过程

    这篇文章主要介绍了从零搭建react+ts组件库(封装antd),实际上,代码开发过程中,还有很多可以辅助开发的模块、流程,本文所搭建的整个项目,我都按照文章一步一步进行了git提交,开发小伙伴可以边阅读文章边对照git提交一步一步来看
    2022-05-05
  • react native与webview通信的示例代码

    react native与webview通信的示例代码

    本篇文章主要介绍了react native与webview通信的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • React useEffect异步操作常见问题小结

    React useEffect异步操作常见问题小结

    本文主要介绍了React useEffect异步操作常见问题小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06
  • React 组件间的通信示例

    React 组件间的通信示例

    这篇文章主要介绍了React 组件间的通信示例,主要通信划分为三种,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-06-06

最新评论