Just Redux:完整指南

2025-05-28

Just Redux:完整指南

封面图片由Josh WeirickUnsplash上提供。

嘿!👋

如果您是前端开发人员或渴望成为前端开发人员,我相信您现在可能已经遇到过 Redux。

你可能知道 Redux 是什么,也可能不知道。也许你已经用了很久了,但还没完全理解。你新建一个项目,然后从别处复制一堆东西,然后就设置好了。说实话,我以前也这么做过。我对 Redux 的所有内容以及需要哪些准备工作都略知一二。到目前为止,这种方法对我来说还算有效,但有时也会遇到一些需要更多知识才能理解的问题。

🙇‍♂️ 因此,我决定研究一下 Redux API。我看了很多在线视频,也阅读了文档。同时,我还写了这篇文章。

🤯 令我惊讶的是,我发现我们在 Redux 中做的 80% 到 90% 的事情都只是简单的 JS。它们只是对象和函数。如果你觉得 Redux 很复杂,你可能需要回到 JS 基础。但如果你在 JS 方面很有信心,那么 Redux 并不难。

⚠️ 在开始之前,我想声明一下,本文仅涵盖 Redux。本文不涉及 React 或任何其他框架,以及它们与 Redux 的交互。

👨‍💻 为了充分利用本文,您可以跟着一起写代码。我为我们将要讨论的所有内容都添加了代码片段。

🤨什么是 Redux?

好吧,如果你点开了这篇文章,我敢肯定你已经知道答案了。但为了解答这个问题,我们还是来做个简单的介绍吧。

Redux 是一个状态管理库。它存储应用程序的状态并提供与该状态交互的方法。它可以与任何框架(例如 React、Angular、Vue 等)一起使用。

安装

npm install redux
Enter fullscreen mode Exit fullscreen mode

对于本文,我们只需要安装redux,不需要其他任何东西。


Redux API 表面仅带有 5 种方法。

截图于 2021-09-22 下午 4.55.33

我们将详细研究其中的每一个。

👉compose

这个方法和 Redux 根本没关系,它的目的是把多个函数捆绑成一个。

假设我们有 3 个数学函数:halfsquaredouble

如果我们想要按顺序应用所有这三种操作,我们需要执行以下操作:

const double = (num) => num * 2;
const square = (num) => num * num;
const half = (num) => num / 2;

const halfSquareDouble = (num) => half(square(double(num)));

console.log(halfSquareDouble(2)); // 8
Enter fullscreen mode Exit fullscreen mode

但我们可以使用更简洁的方式实现同​​样的目的compose

import { compose } from "redux";

const double = (num) => num * 2;
const square = (num) => num * num;
const half = (num) => num / 2;

const halfSquareDouble = compose(half, square, double);

console.log(halfSquareDouble(2)); // 8
Enter fullscreen mode Exit fullscreen mode

compose将把我们所有的功能组合成一个功能。

🧪注意: compose将从右端开始拾取函数。这意味着,如果顺序为compose(half, double, square)4,则相同调用的结果将为 4。

👉createStore

此方法创建 Redux 存储。它接受一个必填参数reducer,以及两个可选参数 - preloadedState(也称为initialState)和enhancer

那么,什么是 Reducer?简单来说,Reducer 只是一个纯函数,它接受两个参数 -state和 ,action并返回一个值,即 new state

截图于 2021-09-23 下午 6.34.49

可以这样理解:存在一个完美的世界/模拟,它存在于某个stateX 中。某些事情发生了;某些事情action被执行了。我们不关心这些行为发生在哪里,也不关心谁是罪魁祸首。我们只知道发生了某些事情,并且这可能会改变我们世界的状态。reducer 的工作就是找出新的stateY。

const reducer = (state, action) => {
  return state
}
Enter fullscreen mode Exit fullscreen mode

这是您可以创建的最简单的减速器。

当我们调用createStore方法时,它会返回一个对象。

import { createStore } from 'redux'

const reducer = (state, action) => {
  return state
}

const initialState = { value: 0 }

const store = createStore(reducer, initialState)
Enter fullscreen mode Exit fullscreen mode

该对象有 4 种方法:

1️⃣ getState:此方法用于获取应用程序的状态。

console.log(store.getState()) // { value: 0 } 
Enter fullscreen mode Exit fullscreen mode

2️⃣ subscribe:此方法用于订阅我们商店的更改。将一个函数传递给此方法,它将在状态更改时被调用。

store.subscribe(() => console.log("State changed!"))
Enter fullscreen mode Exit fullscreen mode

3️⃣ dispatch:此方法用于调度操作。操作会根据应用的当前状态进入 Reducer,并可能更新状态。

🏌️‍♂️ 我们在这里又引入了一个术语 - action,让我们来讨论一下它。

如果你还记得,Reducer 会执行 action 来更新状态。action 会告诉 Reducer 刚刚发生了某件事。可以是用户点击按钮、用户登录、用户添加产品等等。任何旨在改变应用状态的操作都属于 action。

当然,我们完全掌控它们。我们才是定义它们的人。那么如何创建它们呢?嗯,你应该遵循一种特定的风格。

const incrementAction = {
  type: 'INCREMENT'
}
Enter fullscreen mode Exit fullscreen mode

动作本质上是带有type键的对象。就是这样。它也可以有其他键,但type键是必需的。

现在让我们重构我们的 reducer 来利用这个动作。

const reducer = (state = initialState, action) => {
  if (action.type === 'INCREMENT') {
    return { value: state.value + 1 }
  }

  return state
} 
Enter fullscreen mode Exit fullscreen mode

在第一行,我们添加了intialState一个默认参数。通过这样做,我们可以将其从createStore()调用中移除。这实际上是一种最佳实践。

在第 2 行,我们检查收到的操作是否属于类型INCREMENT

在第 3 行,我们正在准备新的状态。这一点很重要。切勿直接修改状态。始终返回一个新创建的对象。如果不这样做,对状态对象的引用将不会改变,您的应用也不会收到有关更改的通知。

state.value++ // 🙅‍♂️ DON'T DO THIS
return { value: state.value + 1 } // 👍 WORKS FINE
Enter fullscreen mode Exit fullscreen mode

在第 4 行,我们最终返回了旧状态,以防我们找不到匹配的 action。这一点也很重要。你的 Reducer 应该始终返回一个 state

现在,我们的 reducer 已经更新,让我们来发送一个动作。

import { createStore } from "redux";

const initialState = { value: 0 };

const incrementAction = {
  type: "INCREMENT"
};

const reducer = (state = initialState, action) => {
  if (action.type === "INCREMENT") {
    return { value: state.value + 1 };
  }

  return state;
};

const store = createStore(reducer);

console.log(store.getState()); // { value: 0 }

store.dispatch(incrementAction);

console.log(store.getState()); // { value: 1 }
Enter fullscreen mode Exit fullscreen mode

如果我们想增加 5 怎么办?我现在做不到。但仔细想想,到目前为止我们写的都是一些基本的 JavaScript 代码,你可能已经了解了。我们可以稍微扩展一下代码,实现目标。

还记得动作可以有其他按键吗?我们将再创建一个动作。

import { createStore } from "redux";

const initialState = { value: 0 };

const incrementAction = {
  type: "INCREMENT"
};

const addAction = {
  type: "ADD",
  payload: 5,
}

const reducer = (state = initialState, action) => {
  if (action.type === "INCREMENT") {
    return { value: state.value + 1 };
  }

  if (action.type === "ADD") {
    return { value: state.value + action.payload }
  }

  return state;
};

const store = createStore(reducer);

store.dispatch(addAction)

console.log(store.getState()) // { value: 5 }
Enter fullscreen mode Exit fullscreen mode

好的!到目前为止一切都很好。但是 5 还不够,我们再创建一个 10 的,然后再创建一个 100 的?感觉很蠢!我们不可能覆盖所有数字。

好的!如果我们这样做会怎么样?

store.dispatch({ type: "ADD", payload: 5 })
store.dispatch({ type: "ADD", payload: 10 })
store.dispatch({ type: "ADD", payload: 100 })
Enter fullscreen mode Exit fullscreen mode

是的!这确实完成了任务,但可扩展性不强。如果之后我们决定用INCREASE_BY而不是 来调用它ADD,那么我们就必须在所有地方都更新它。而且,我们有可能创建了一个类型,结果却写成了INCRAESE_BY。祝你好运,别再找错字了!😬

有一种优雅的方法可以解决这个问题:使用 Action Creators。

🤖 动作创建器只是为您创建动作的功能。

const add = (number) => {
  return {
    type: "ADD",
    payload: number
  }
}

store.dispatch(add(5))
store.dispatch(add(10))
store.dispatch(add(100))
Enter fullscreen mode Exit fullscreen mode

我们创建了一个add返回 action 对象的函数。我们可以在任何地方调用它,它会为我们创建一个 action 对象。

该解决方案更加清洁并且被广泛使用。

我们更新后的代码现在如下所示:

import { createStore } from "redux";

const initialState = { value: 0 };

// constants
const INCREMENT = "INCREMENT";
const ADD = "ADD";

// action creators
const increment = () => ({ type: INCREMENT });
const add = (number) => ({ type: ADD, payload: number });

const reducer = (state = initialState, action) => {
  if (action.type === INCREMENT) {
    return { value: state.value + 1 };
  }
  if (action.type === ADD) {
    return { value: state.value + action.payload };
  }
  return state;
};

const store = createStore(reducer);

console.log(store.getState()); // { value: 0 }

store.dispatch(increment());
store.dispatch(add(2));

console.log(store.getState()); // { value: 3 }
Enter fullscreen mode Exit fullscreen mode

注意,我们将"INCREMENT"和存储"ADD"为常量。这是因为我们在 Reducer 中重复使用了它们,因此存在拼写错误的风险。将 Action 类型存储为常量并集中存放是一个好习惯。


🎉 如果你已经读到这里,恭喜你!凭借你目前掌握的所有知识,你可以开始使用 Redux 创建应用了。当然,还有很多内容需要学习,但你已经掌握了相当一部分 API。干得漂亮!


4️⃣ replaceReducer:此方法用于将当前的根 Reducer 函数替换为新的。调用此方法将更改内部 Reducer 函数的引用。当你为了提高性能而拆分代码时,这会发挥作用。

const newRootReducer = combineReducers({
  existingSlice: existingSliceReducer,  
  newSlice: newSliceReducer
});

store.replaceReducer(newRootReducer);
Enter fullscreen mode Exit fullscreen mode

👉bindActionCreators

现在我们对动作创建者和调度有了一些了解,我们可以讨论这种方法了。

dispatch(increment())
dispatch(add(5))
Enter fullscreen mode Exit fullscreen mode

到目前为止,这就是我们调度操作的方式。但还有一种更简单的方法。

const actions = bindActionCreators({ add, increment }, store.dispatch)

actions.increment()
actions.add(4)
Enter fullscreen mode Exit fullscreen mode

bindActionCreators接受两个参数:

  1. 一个包含所有动作创建者的对象。
  2. 我们想要将我们的动作创建者绑定到的方法。

它返回一个对象,看起来与我们传入的第一个参数相同。唯一的区别是,现在我们可以直接调用这些方法,而无需明确调用 dispatch。

这样做有什么好处?

bindActionCreators 的唯一用例是当你想将一些 action creators 传递给一个不知道 Redux 的组件,并且你不想将 dispatch 或 Redux store 传递给它。 - Redux 文档

另外,请注意,我们所做的只是普通的 JS,我们可以通过编写自己的函数将动作创建者绑定到 dispatch; 来实现相同的结果,而无需调用bindActionCreators

👉combineReducers

当你开发一个需要隔离数据的大型应用时,使用多个 Reducer 来降低复杂性是合理的。此方法会将所有这些小型 Reducer 组合起来,并返回一个可供我们createStore方法使用的 Reducer,通常称为根 Reducer。

首先,我们来看看为什么我们需要多个 Reducer。请考虑以下代码。

import { createStore } from "redux";

// constants
const CHANGE_USER_EMAIL = "CHANGE_USER_EMAIL";
const ADD_PRODUCT = "ADD_PRODUCT";

// action creators
const changeUserEmail = (email) => ({
  type: CHANGE_USER_EMAIL,
  payload: { email }
});
const addProduct = (product) => ({
  type: ADD_PRODUCT,
  payload: { product }
});

const initialState = {
  user: {
    name: "Mark",
    email: "mark@facebook.com"
  },
  cart: {
    products: []
  }
};

const reducer = (state = initialState, action) => {
  if (action.type === CHANGE_USER_EMAIL) {
    return {
      ...state,
      user: {
        ...state.user,
        email: action.payload.email
      }
    };
  }
  if (action.type === ADD_PRODUCT) {
    return {
      ...state,
      cart: {
        ...state.cart,
        products: [...state.cart.products, action.payload.product]
      } 
    };
  }
  return state;
};

const store = createStore(reducer);

console.log(store.getState()); 
// { user: { name: 'Mark', email: 'mark@facebook.com' }, cart: { products: [] } }

store.dispatch(changeUserEmail("mark@instagram.com"));

console.log(store.getState()); 
// { user: { name: 'Mark', email: 'mark@instagram.com' }, cart: { products: [] } }
Enter fullscreen mode Exit fullscreen mode

我们可以看到,这个 Reducer 已经看起来有点复杂了。随着应用规模的增长,数据嵌套的层次会越来越深,Reducer 的大小也会随之增大。

仔细想想,usercart是两个完全不同的数据点。我们可以将它们拆分到两个不同的 Reducer 中。我们开始吧。

const initialState = {
  user: {
    name: "Mark",
    email: "mark@facebook.com"
  },
  cart: {
    products: []
  }
};

const userReducer = (user = initialState.user, action) => {
  if (action.type === CHANGE_USER_EMAIL) {
    return {
      ...user,
      email: action.payload.email
    };
  }
  return user;
}

const cartReducer = (cart = initialState.cart, action) => {
  if (action.type === ADD_PRODUCT) {
    return {
      ...cart,
      products: [...cart.products, action.payload.product]
    };
  }
  return cart;
}
Enter fullscreen mode Exit fullscreen mode

现在我们有两个简单的 Reducer,代码看起来很简洁。但是createStore只接受一个 Reducer,我们应该传递哪一个呢?

两者皆可。使用combineReducers

const rootReducer = combineReducers({
  user: userReducer,
  cart: cartReducer
});

const store = createStore(rootReducer);
Enter fullscreen mode Exit fullscreen mode

此方法接受一个对象,其中的键可以是任何值,但值必须是我们的 Reducer。它将返回一个可以传递给 的 Reducer createStore

我们的完整代码现在看起来像这样。

import { combineReducers, createStore } from "redux";

// constants
const CHANGE_USER_EMAIL = "CHANGE_USER_EMAIL";
const ADD_PRODUCT = "ADD_PRODUCT";

// action creators
const changeUserEmail = (email) => ({
  type: CHANGE_USER_EMAIL,
  payload: { email }
});
const addProduct = (product) => ({
  type: ADD_PRODUCT,
  payload: { product }
});

const initialState = {
  user: {
    name: "Mark",
    email: "mark@facebook.com"
  },
  cart: {
    products: []
  }
};

const userReducer = (user = initialState.user, action) => {
  if (action.type === CHANGE_USER_EMAIL) {
    return {
      ...user,
      email: action.payload.email
    };
  }
  return user;
};

const cartReducer = (cart = initialState.cart, action) => {
  if (action.type === ADD_PRODUCT) {
    return {
      ...cart,
      products: [...cart.products, action.payload.product]
    };
  }
  return cart;
};

const rootReducer = combineReducers({
  user: userReducer,
  cart: cartReducer
});

const store = createStore(rootReducer);

console.log(store.getState()); 
// { user: { name: 'Mark', email: 'mark@facebook.com' }, cart: { products: [] } }

store.dispatch(changeUserEmail("mark@instagram.com"));

console.log(store.getState()); 
// { user: { name: 'Mark', email: 'mark@instagram.com' }, cart: { products: [] } }
Enter fullscreen mode Exit fullscreen mode

🌸 商店增强功能

如果你还记得的话,createStore它接受一个可选参数 - enhancers
增强器其实就是高阶函数。它们为我们的 store 添加了一些额外的功能。例如,Redux dev tools 就是一个增强器。

我们这里不会过多讨论增强器,因为我们很少会创建新的增强器。我们会在另一篇文章中详细讨论这个问题。

⛓ 中间件

中间件使我们能够拦截操作,并在操作到达 Reducer 之前执行我们想要的操作。我们可以记录操作、记录存储状态、记录崩溃报告等等。

让我们创建一个中间件,用于在分派动作时记录动作。

const logger = (store) => (next) => (action) => {
  console.log("DISPATCHED ACTION: ", action);
  next(action);
}
Enter fullscreen mode Exit fullscreen mode

这是您可以创建的最简单的中间件之一。它记录操作,然后将调用转发给管道中的其他中间件和 Reducer。

但是我们如何使用我们新创建的中间件?

👉applyMiddleware

此方法将接受一组中间件并返回一个增强器。增强器会进入createStore函数调用。

import { applyMiddleware, createStore } from 'redux'

const logger = (store) => (next) => (action) => {
  console.log("DISPATCHED ACTION: ", action);
  next(action);
}

const store = createStore(rootReducer, applyMiddleware(logger));
Enter fullscreen mode Exit fullscreen mode

现在,每次我们发送一个动作时,我们的控制台中都会有一个日志。

🎉 至此,我们讲解了 Redux 中的 final 方法。Redux API 的全部内容都讲完了。

✨ 如果不告诉大家现代 Redux 与本文讨论的不同,那就太不公平了。基本概念仍然适用,但代码量会减少。感谢 Redux 团队。🙏 想了解吗?点击这里


💡 我知道你现在还无法掌握 100% 的知识,但了解你掌握的所有工具总是好的。

💾 这篇文章可以作为你在面试前或实施过程中复习某些内容的指南。保存起来!

🤝 如果您想阅读我即将发表的更多文章,您可以通过LinkedInTwitter与我联系。

🙏 感谢您的阅读!

文章来源:https://dev.to/thesanjeevsharma/just-redux-the-complete-guide-44d5
PREV
实时更新:轮询、SSE 和 Web 套接字
NEXT
向我的 Uber 司机解释 Kubernetes 对话开始 结论