Redux 原理探秘

简介: Redux 是一个非常不错的状态管理库,和 Vuex 不同的是 Redux 并不和 React 强绑定,你甚至可以在 Vue 中使用 Redux。当初的目标是创建一个状态管理库,来提供最简化 API。

Redux 是一个非常不错的状态管理库,和 Vuex 不同的是 Redux 并不和 React 强绑定,你甚至可以在 Vue 中使用 Redux。当初的目标是创建一个状态管理库,来提供最简化 API。


复习


具体 Redux 的使用细节我们不再啰嗦,网上有很多教程,这里只使用Redux 最基本的用法。

首先实现 store

import { Action, legacy_createStore as createStore } from "redux";
const reducer = (state = 0, action: Action) => {
  switch (action.type) {
    case 'ADD':
      return state + 1
    case 'SUB':
      return state - 1
    default:
      return state
  }
}
const store = createStore(reducer)
export default store
复制代码

createStore 已经被标记为废弃,但是提供了一个 legacy_createStore 方法用于兼容之前的代码,相较之下官方更推荐使用 RTK

然后在页面中使用store,这里我们先复习一下 store 对象的几个常用 API

  • getState:获取 store 的当前状态;
  • dispatch:传递一个 Action 对象作为参数,用于修改状态;
  • subscribe:订阅,用于在状态发生改变时执行传入的回调(例如更新渲染),返回一个函数,用于取消订阅。
import store from './store'
class Count extends Component {
  unsubscribe: (() => void) | undefined;
  componentDidMount() {
    // 组件挂载时订阅 store
    this.unsubscribe = store.subscribe(() => {
      this.forceUpdate()
    })
  }
  componentWillUnmount() {
    // 卸载组件时取消订阅
    this.unsubscribe!()
  }
  add = () => {
    store.dispatch({ type: 'ADD' })
  }
  sub = () => {
    store.dispatch({ type: 'SUB' })
  }
  render() {
    return <div>
      {store.getState()}
      <button onClick={this.add}>+</button>
      <button onClick={this.sub}>-</button>
    </div>
  }
}
复制代码

现在我们的页面上就可以看到效果了,点击对应的按钮数据就会发生相应的改变

1682520769(1).png


实现一个丐版


我们基于上面的复习代码作为测试用例,我们来手动实现这几个常用的 API。首先在 src 下创建目录 /src/lib/redux,新建 index.ts 文件

export function legacy_createStore
  <S, A extends Action>(reducer: Reducer<S, A>) {
  let currentState: S;
  let listeners: Array<() => void> = [];
  function getState() {
  }
  function dispatch<T extends A>(action: T) {
  }
  function subscribe(listener: () => void) {
  }
  return {
    getState,
    dispatch,
    subscribe
  }
}
复制代码

getState 很简单,只需要把 currentState 返回即可;subscribe 需要将传入的回调函数保存到 linsteners 中,然后把移除回调的函数返回即可。

function getState() {
  return currentState
}
function subscribe(listener: () => void) {
  listeners.push(listener);
  return () => {
    const index = listeners.indexOf(listener);
    listeners.splice(index, 1);
  }
}
复制代码

在处理 dispatch 的时候,要注意 dispatch 是有返回值的,返回值是传入的 action,我们跟随 redux 一样实现即可。

1682520795(1).png

function dispatch<T extends A>(action: T) {
  currentState = reducer(currentState, action);
  listeners.forEach(listener => listener())
  return action;
}
复制代码

此时三个常用的 API 已经完成,此时将组件中引用的 redux 替换为我们刚才在本地实现的 redux,打开浏览器会看到原来显示的 0 并没有成功想显示,但是点击两个按钮对应的值却正确的显示了出来。

这是因为我们刚才的逻辑中没有给 currentState 赋初始值,我们只需要在刚才的函数中触发一次 dispatch 即可,这里传入的 action.type 写一个用户不会写的值集即可,例如当前的日期。

dispatch({ type: new Date().toUTCString() })
复制代码

此时页面实现的效果就跟我们之前用官方redux 实现的效果一样了。


增强:中间件


目前为止 redux 的 dispatch 只支持最基础的对象作为 action,如果你想使用函数作为 action,会在控制台看到报错

sub = () => {
  // @ts-expect-error
  store.dispatch(dispatch => {
    setTimeout(() => {
      dispatch({type: "SUB"});
    }, 1000);
  });
}
复制代码

1682520821(1).png

这时候 redux 会提示你是否需要为 redux 安装中间件,这里我们为 redux 安装 thunk 和 logger 两个中间件

$ npm install redux-thunk redux-logger
复制代码

由于 redux-logger 本身没有生命类型,所以在使用ts 时会抛出错误,我们需要额外为 logger 安装类型包

$ npm install @types/redux-logger -D
复制代码

安装完类型包之后我们需要在 tsconfig.json 中添加类型,否则编译时还是无法解析类型

{
  "compilerOptions": {
    "types": [
      "redux-logger"
    ]
  },
}
复制代码

此时我们就可以为 redux 添加中间件了

import { Action, applyMiddleware } from "redux";
import { legacy_createStore as createStore } from "redux";
import thunk from "redux-thunk";
import logger from 'redux-logger';
const reducer = (state = 0, action: Action) => {
  switch (action.type) {
    case 'ADD':
      return state + 1
    case 'SUB':
      return state - 1
    default:
      return state
  }
}
const store = createStore(reducer, applyMiddleware(thunk, logger))
export default store
复制代码

此时将我们在页面中点击按钮,不会再抛出异常

1682520849(1).png

在了解了中间件的使用方法之后,我们也给我们的丐版 redux 添加中间件机制。

上一小节的 Reducer 其实是函数式编程概念中的纯函数,函数式编程中还有另一个概念——柯里化,这里我们就不再强调,不了解的可以百度。

先来看和一个示例

function f1(arg: any) {
  console.log('f1', arg);
  return arg;
}
function f2(arg: any) {
  console.log('f2', arg);
  return arg;
}
function f3(arg: any) {
  console.log('f3', arg);
  return arg;
}
复制代码

我们想让f1函数的返回值作为f2的参数,再让 f2 的返回值作为 f3的参数,按照最简单的方法,我们可以嵌套调用

f3(f2(f1('function')))
复制代码

这样虽然可以实现我们要的效果,但是如果函数多了的时候就很不方便,我们可以利用柯里化,让函数自动完成嵌套

const compose = (...fns: Function[]) => {
  if (fns.length === 0) {
    return (arg: any) => arg;
  }
  if (fns.length === 1) {
    return fns[0];
  }
  return fns.reduce(
    (fn1, fn2) =>
      (...args: any[]) =>
        fn1(fn2(...args)),
  );
};
compose(f1, f2, f3)('function')
// f3 function
// f2 function
// f1 function
复制代码

我们利用数组的 reduce 方法,将函数依次进行嵌套,这其实就是 redux 中 middleware 的实现方式。

现在有没有感觉很眼熟,我们刚才引入的 thunk 和 logger其实就是一个函数,我们引入中间件的方式就跟这里调用 compose 类似。

我们来自己实现一个 applyMiddleware,我们需要改写原有的 dispatch,让其可以通过 middleware 来增强功能。

export function applyMiddleware(...middlewares: Middleware[]) {
  return (createStore: typeof legacy_createStore) => (reducer: Reducer) => {
    const store = createStore(reducer)
    let dispatch = store.dispatch
    const midAPI = {
      getState: store.getState,
      dispatch: (action: Action, ...args: any[]) => dispatch(action, ...args)
    }
    // 给 middleware 传入新的 dispatch
    const middlewareChain = middlewares
      .map(middleware => middleware(midAPI))
    // 增强 dispatch(把所有中间件函数都执行)
    dispatch = compose(...middlewareChain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
};
复制代码

这时替换掉import 的位置,回到页面可以看到效果和刚才一模一样。

相关文章
|
设计模式 安全 测试技术
【C/C++ 设计模式 单例】单例模式的选择策略:何时使用,何时避免
【C/C++ 设计模式 单例】单例模式的选择策略:何时使用,何时避免
251 0
|
Java
java8中修改parallelStream默认并发数
java8中修改parallelStream默认并发数
2289 0
java8中修改parallelStream默认并发数
|
存储 安全 Java
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(进阶篇)
本文是Java基础的进阶篇,对异常、集合、泛型、Java8新特性、I/O流等知识进行深入浅出的介绍,并附有对应的代码示例,重要的地方带有对性能、底层原理、源码的剖析。适合Java初学者。
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(进阶篇)
|
弹性计算 数据可视化 Ubuntu
ECS如何安装可视化桌面
【1月更文挑战第10天】ECS如何安装可视化桌面
656 5
|
机器学习/深度学习 PyTorch 编译器
Pytorch的编译新特性TorchDynamo的工作原理和使用示例
PyTorch的TorchDynamo是一个即时编译器,用于优化动态图执行,提高运行效率。它在运行时分析和转换代码,应用优化技术,如操作符融合,然后编译成高效机器码。通过一个包含特征工程、超参数调整、交叉验证的合成数据集示例,展示了TorchDynamo如何减少训练时间并提高模型性能。它易于集成,只需对现有PyTorch代码进行小改动,即可利用其性能提升。TorchDynamo的优化包括动态捕获计算图、应用优化和编译,适用于实时应用和需要快速响应的场景。
252 11
|
Java Maven
Java系列之:GroupId和ArtifactId的作用
这篇文章解释了在Maven项目中`GroupId`和`ArtifactId`的含义和作用,其中`GroupId`通常由域和自定义域名组成,用于区分组织或公司,而`ArtifactId`是项目或模块的名称,两者结合用于唯一标识项目包,确保不会与他人的项目包重复。
|
数据采集 API 索引
异步任务处理系统问题之异步任务处理系统的问题如何解决
异步任务处理系统问题之异步任务处理系统的问题如何解决
315 2
|
小程序 JavaScript Java
基于微信小程序便捷记账系统设计与实现(源码+lw+部署文档+讲解等)
基于微信小程序便捷记账系统设计与实现(源码+lw+部署文档+讲解等)
401 8
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的党建信息管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的党建信息管理系统附带文章源码部署视频讲解等
116 1
|
消息中间件 Shell Linux
RabbitMQ部署指南
主要是如何部署RabbitMQ的具体步骤

热门文章

最新文章