跳至主要内容

Redux 基础知识,第 4 部分:Store

Redux 基础知识,第 4 部分:Store

您将学到什么
  • 如何创建 Redux store
  • 如何使用 store 更新状态并监听更新
  • 如何配置商店以扩展其功能
  • 如何设置 Redux DevTools 扩展以调试您的应用程序

介绍

第 3 部分:状态、操作和 reducer 中,我们开始编写示例待办事项应用程序。我们列出了业务需求,定义了使应用程序正常运行所需的状态结构,并创建了一系列操作类型来描述“发生了什么”并匹配用户与我们的应用程序交互时可能发生的事件类型。我们还编写了可以处理更新我们 state.todosstate.filters 部分的reducer 函数,并了解了如何使用 Redux combineReducers 函数根据我们应用程序中每个功能的不同“切片 reducer”来创建“根 reducer”。

现在,是时候将这些部分整合在一起了,Redux 应用程序的核心部分:商店

注意

请注意,本教程有意展示旧式 Redux 逻辑模式,这些模式需要比我们今天作为构建 Redux 应用程序的正确方法教授的“现代 Redux”模式(使用 Redux Toolkit)更多的代码,以便解释 Redux 背后的原理和概念。它旨在成为一个生产就绪的项目。

查看以下页面了解如何使用 Redux Toolkit 使用“现代 Redux”

Redux 商店

Redux 商店将构成您的应用程序的状态、操作和 reducer 整合在一起。商店有几个职责

重要的是要注意,在 Redux 应用程序中,您只会拥有一个商店。当您想要拆分数据处理逻辑时,您将使用 reducer 组合 并创建多个可以组合在一起的 reducer,而不是创建单独的商店。

创建商店

每个 Redux store 都有一个单一的根 reducer 函数。在上一节中,我们使用 combineReducers 创建了一个根 reducer 函数。该根 reducer 目前在我们的示例应用程序中的 src/reducer.js 中定义。让我们导入该根 reducer 并创建我们的第一个 store。

Redux 核心库有一个createStore API,它将创建 store。添加一个名为 store.js 的新文件,并导入 createStore 和根 reducer。然后,调用 createStore 并传入根 reducer

src/store.js
import { createStore } from 'redux'
import rootReducer from './reducer'

const store = createStore(rootReducer)

export default store

加载初始状态

createStore 也可以接受一个 preloadedState 值作为它的第二个参数。你可以使用它在创建 store 时添加初始数据,例如从服务器发送的 HTML 页面中包含的值,或者在用户再次访问页面时持久化到 localStorage 并读取回来,就像这样

storeStatePersistenceExample.js
import { createStore } from 'redux'
import rootReducer from './reducer'

let preloadedState
const persistedTodosString = localStorage.getItem('todos')

if (persistedTodosString) {
preloadedState = {
todos: JSON.parse(persistedTodosString)
}
}

const store = createStore(rootReducer, preloadedState)

分发 Actions

现在我们已经创建了一个 store,让我们验证我们的程序是否正常工作!即使没有任何 UI,我们也可以测试更新逻辑。

提示

在运行此代码之前,尝试回到 src/features/todos/todosSlice.js,并从 initialState 中删除所有示例 todo 对象,使其成为一个空数组。这将使此示例的输出更容易阅读。

src/index.js
// Omit existing React imports

import store from './store'

// Log the initial state
console.log('Initial state: ', store.getState())
// {todos: [....], filters: {status, colors}}

// Every time the state changes, log it
// Note that subscribe() returns a function for unregistering the listener
const unsubscribe = store.subscribe(() =>
console.log('State after dispatch: ', store.getState())
)

// Now, dispatch some actions

store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about reducers' })
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about stores' })

store.dispatch({ type: 'todos/todoToggled', payload: 0 })
store.dispatch({ type: 'todos/todoToggled', payload: 1 })

store.dispatch({ type: 'filters/statusFilterChanged', payload: 'Active' })

store.dispatch({
type: 'filters/colorFilterChanged',
payload: { color: 'red', changeType: 'added' }
})

// Stop listening to state updates
unsubscribe()

// Dispatch one more action to see what happens

store.dispatch({ type: 'todos/todoAdded', payload: 'Try creating a store' })

// Omit existing React rendering logic

记住,每次我们调用 store.dispatch(action)

  • store 会调用 rootReducer(state, action)
    • 该根 reducer 可能会在自身内部调用其他 slice reducer,例如 todosReducer(state.todos, action)
  • store 会将新的 state 值保存到内部
  • store 会调用所有监听器订阅回调
  • 如果监听器可以访问 store,它现在可以调用 store.getState() 来读取最新的 state 值

如果我们查看该示例的控制台日志输出,你可以看到 Redux state 在每个 action 分发时是如何变化的

Logged Redux state after dispatching actions

请注意,我们的应用程序没有从最后一个 action 中记录任何内容。这是因为我们在调用 unsubscribe() 时删除了监听器回调,所以 action 分发后没有其他内容运行。

我们在开始编写 UI 之前就指定了应用程序的行为。这有助于我们相信应用程序将按预期工作。

信息

如果你想,现在可以尝试为你的 reducer 编写测试。因为它们是 纯函数,测试它们应该很简单。用示例 stateaction 调用它们,获取结果,并检查它是否与预期相符。

todosSlice.spec.js
import todosReducer from './todosSlice'

test('Toggles a todo based on id', () => {
const initialState = [{ id: 0, text: 'Test text', completed: false }]

const action = { type: 'todos/todoToggled', payload: 0 }
const result = todosReducer(initialState, action)
expect(result[0].completed).toBe(true)
})

Redux Store 内部

看看 Redux store 的内部工作原理可能会有所帮助。这是一个大约 25 行代码的工作 Redux store 的简化示例。

miniReduxStoreExample.js
function createStore(reducer, preloadedState) {
let state = preloadedState
const listeners = []

function getState() {
return state
}

function subscribe(listener) {
listeners.push(listener)
return function unsubscribe() {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}

function dispatch(action) {
state = reducer(state, action)
listeners.forEach(listener => listener())
}

dispatch({ type: '@@redux/INIT' })

return { dispatch, subscribe, getState }
}

这个简化版的 Redux store 运行良好,你可以用它来替换你一直在应用程序中使用的实际 Redux createStore 函数。(试试看!)实际的 Redux store 实现更长,也更复杂,但大部分是注释、警告信息和处理一些边缘情况。

如你所见,这里的实际逻辑相当短。

  • store 在其内部包含当前的 state 值和 reducer 函数。
  • getState 返回当前的 state 值。
  • subscribe 保持一个监听回调数组,并返回一个用于移除新回调的函数。
  • dispatch 调用 reducer,保存 state,并运行监听器。
  • store 在启动时分派一个 action,以使用它们的 state 初始化 reducer。
  • store API 是一个包含 {dispatch, subscribe, getState} 的对象。

特别强调其中一个:注意 getState 只是返回当前的 state 值。这意味着默认情况下,没有任何东西可以阻止你意外地修改当前的 state 值!这段代码将运行而不会出现任何错误,但它是错误的。

const state = store.getState()
// ❌ Don't do this - it mutates the current state!
state.filters.status = 'Active'

换句话说

  • Redux store 在你调用 getState() 时不会创建 state 值的额外副本。它与从根 reducer 函数返回的引用完全相同。
  • Redux store 不会做任何其他事情来防止意外修改。你可以修改 state,无论是在 reducer 内部还是在 store 外部,你必须始终小心避免修改。

意外修改的一个常见原因是排序数组。调用 array.sort() 实际上会修改现有的数组。如果我们调用 const sortedTodos = state.todos.sort(),我们最终会无意中修改真正的 store state。

提示

第 8 部分:现代 Redux 中,我们将看到 Redux Toolkit 如何帮助避免 reducer 中的修改,以及如何检测和警告 reducer 外部的意外修改。

配置 Store

我们已经了解到可以将 rootReducerpreloadedState 参数传递给 createStore。但是,createStore 还可以接受一个额外的参数,用于自定义存储的功能并赋予其新的能力。

Redux 存储通过称为 **存储增强器** 的东西进行自定义。存储增强器就像 createStore 的特殊版本,它在原始 Redux 存储周围添加了另一层包装。增强存储可以通过提供自己的 dispatchgetStatesubscribe 函数版本来更改存储的行为,而不是使用原始版本。

在本教程中,我们不会详细介绍存储增强器的工作原理,而是重点介绍如何使用它们。

使用增强器创建存储

我们的项目在 src/exampleAddons/enhancers.js 文件中提供了两个小型示例存储增强器

  • sayHiOnDispatch:一个增强器,每次调度操作时都会在控制台中始终记录 'Hi'!
  • includeMeaningOfLife:一个增强器,始终将字段 meaningOfLife: 42 添加到从 getState() 返回的值中

让我们从使用 sayHiOnDispatch 开始。首先,我们将导入它,并将其传递给 createStore

src/store.js
import { createStore } from 'redux'
import rootReducer from './reducer'
import { sayHiOnDispatch } from './exampleAddons/enhancers'

const store = createStore(rootReducer, undefined, sayHiOnDispatch)

export default store

这里我们没有 preloadedState 值,因此我们将 undefined 作为第二个参数传递。

接下来,让我们尝试调度一个操作

src/index.js
import store from './store'

console.log('Dispatching action')
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
console.log('Dispatch complete')

现在看看控制台。您应该在其他两个日志语句之间看到 'Hi!' 记录在那里

sayHi store enhancer logging

sayHiOnDispatch 增强器用它自己的 dispatch 特殊版本包装了原始 store.dispatch 函数。当我们调用 store.dispatch() 时,实际上是在调用 sayHiOnDispatch 中的包装函数,该函数调用了原始函数,然后打印了 'Hi'。

现在,让我们尝试添加第二个增强器。我们可以从同一个文件中导入 includeMeaningOfLife ... 但是我们遇到了一个问题。createStore 只能接受一个增强器作为它的第三个参数! 我们如何同时传递两个增强器?

我们真正需要的是某种方法将 sayHiOnDispatch 增强器和 includeMeaningOfLife 增强器合并成一个组合的增强器,然后传递它。

幸运的是,Redux 核心包含 一个 compose 函数,可用于将多个增强器合并在一起。让我们在这里使用它

src/store.js
import { createStore, compose } from 'redux'
import rootReducer from './reducer'
import {
sayHiOnDispatch,
includeMeaningOfLife
} from './exampleAddons/enhancers'

const composedEnhancer = compose(sayHiOnDispatch, includeMeaningOfLife)

const store = createStore(rootReducer, undefined, composedEnhancer)

export default store

现在我们可以看看使用存储会发生什么

src/index.js
import store from './store'

store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: 'Hi!'

console.log('State after dispatch: ', store.getState())
// log: {todos: [...], filters: {status, colors}, meaningOfLife: 42}

记录的输出如下所示

meaningOfLife store enhancer logging

因此,我们可以看到两个增强器同时修改了存储的行为。sayHiOnDispatch 改变了 dispatch 的工作方式,而 includeMeaningOfLife 改变了 getState 的工作方式。

存储增强器是修改存储的非常强大的方法,几乎所有 Redux 应用程序在设置存储时都会包含至少一个增强器。

提示

如果您没有要传入的 preloadedState,可以将 enhancer 作为第二个参数传入。

const store = createStore(rootReducer, storeEnhancer)

中间件

增强器很强大,因为它们可以覆盖或替换存储的任何方法:dispatchgetStatesubscribe

但是,大多数情况下,我们只需要自定义 dispatch 的行为。如果有一种方法可以在 dispatch 运行时添加一些自定义行为,那就太好了。

Redux 使用一种称为 **中间件** 的特殊插件来让我们自定义 dispatch 函数。

如果您曾经使用过 Express 或 Koa 之类的库,您可能已经熟悉在其中添加中间件以自定义行为的想法。在这些框架中,中间件是您可以在框架接收请求和框架生成响应之间放置的一些代码。例如,Express 或 Koa 中间件可以添加 CORS 标头、日志记录、压缩等。中间件的最佳功能是它可以在链中组合。您可以在单个项目中使用多个独立的第三方中间件。

Redux 中间件解决的问题与 Express 或 Koa 中间件不同,但概念上类似。**Redux 中间件在分派操作和操作到达 reducer 的那一刻之间提供了一个第三方扩展点。**人们使用 Redux 中间件进行日志记录、崩溃报告、与异步 API 交谈、路由等。

首先,我们将看看如何将中间件添加到存储中,然后我们将展示如何编写自己的中间件。

使用中间件

我们已经看到可以使用存储增强器来自定义 Redux 存储。Redux 中间件实际上是在 Redux 自带的非常特殊的存储增强器 **applyMiddleware** 之上实现的。

既然我们已经知道如何在商店中添加增强器,现在我们应该能够做到这一点。我们将从 `applyMiddleware` 本身开始,并将添加三个已包含在此项目中的示例中间件。

src/store.js
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

顾名思义,这些中间件中的每一个都会在调度操作时打印一个数字。

如果我们现在调度会发生什么?

src/index.js
import store from './store'

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

我们可以在控制台中看到输出

print middleware logging

那么它是如何工作的呢?

中间件在商店的 `dispatch` 方法周围形成一个管道。当我们调用 `store.dispatch(action)` 时,我们实际上是在调用管道中的第一个中间件。然后,该中间件可以在看到操作时执行任何它想要的操作。通常,中间件会检查操作是否为它关心的特定类型,就像 reducer 一样。如果类型正确,中间件可能会运行一些自定义逻辑。否则,它会将操作传递给管道中的下一个中间件。

reducer 不同中间件可以在内部具有副作用,包括超时和其他异步逻辑。

在这种情况下,操作将被传递

  1. `print1` 中间件(我们将其视为 `store.dispatch`)
  2. `print2` 中间件
  3. `print3` 中间件
  4. 原始 `store.dispatch`
  5. `store` 内部的根 reducer

由于这些都是函数调用,因此它们都从该调用栈中返回。因此,`print1` 中间件是第一个运行的,也是最后一个完成的。

编写自定义中间件

我们也可以编写自己的中间件。您可能不需要一直这样做,但自定义中间件是为 Redux 应用程序添加特定行为的好方法。

Redux 中间件被写成一系列三个嵌套函数。让我们看看这个模式是什么样的。我们将尝试使用 `function` 关键字编写这个中间件,以便更清楚地了解发生了什么

// Middleware written as ES5 functions

// Outer function:
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` 调用,并接收一个包含商店的 `{dispatch, getState}` 函数的 `storeAPI` 对象。这些是实际上是商店一部分的相同 `dispatch` 和 `getState` 函数。如果您调用此 `dispatch` 函数,它将把操作发送到中间件管道的开头。这仅调用一次。
  • wrapDispatch: 中间函数接收一个名为 next 的函数作为参数。这个函数实际上是管道中的下一个中间件。如果这个中间件是序列中的最后一个,那么 next 实际上是原始的 store.dispatch 函数。调用 next(action) 将操作传递给管道中的下一个中间件。这也只调用一次。
  • handleAction: 最后,内部函数接收当前的 action 作为参数,并且每次调度操作时都会被调用。
提示

您可以随意给这些中间件函数命名,但使用这些名称可以帮助您记住每个函数的作用。

  • 外部:someCustomMiddleware(或您的中间件的任何名称)
  • 中间:wrapDispatch
  • 内部:handleAction

因为这些是普通函数,我们也可以使用 ES2015 箭头函数来编写它们。这使我们可以更简洁地编写它们,因为箭头函数不需要 return 语句,但如果您不熟悉箭头函数和隐式返回,它可能也更难阅读。

以下是与上面相同的示例,使用箭头函数

const anotherExampleMiddleware = storeAPI => next => action => {
// Do something in here, when each action is dispatched

return next(action)
}

我们仍然将这三个函数嵌套在一起,并返回每个函数,但隐式返回使代码更简洁。

您的第一个自定义中间件

假设我们想在应用程序中添加一些日志记录。我们希望在调度操作时在控制台中看到每个操作的内容,并且我们希望看到操作由 reducer 处理后状态是什么。

信息

这些示例中间件不是实际待办事项应用程序的特定部分,但您可以尝试将它们添加到您的项目中,看看使用它们时会发生什么。

我们可以编写一个小的中间件,它将把这些信息记录到控制台中。

const loggerMiddleware = storeAPI => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', storeAPI.getState())
return result
}

每当调度操作时

  • handleAction 函数的第一部分运行,我们打印 'dispatching'
  • 我们将操作传递给 next 部分,它可能是另一个中间件或真正的 store.dispatch
  • 最终 reducer 运行,状态更新,next 函数返回。
  • 我们现在可以调用 storeAPI.getState() 并查看新状态是什么。
  • 最后,我们返回来自 next 中间件的任何 result 值。

任何中间件都可以返回任何值,并且当您调用 `store.dispatch()` 时,实际上返回的是管道中第一个中间件的返回值。例如

const alwaysReturnHelloMiddleware = storeAPI => next => action => {
const originalResult = next(action)
// Ignore the original result, return something else
return 'Hello!'
}

const middlewareEnhancer = applyMiddleware(alwaysReturnHelloMiddleware)
const store = createStore(rootReducer, middlewareEnhancer)

const dispatchResult = store.dispatch({ type: 'some/action' })
console.log(dispatchResult)
// log: 'Hello!'

让我们再举一个例子。中间件通常会寻找特定的操作,然后在调度该操作时执行某些操作。中间件还具有在内部运行异步逻辑的能力。我们可以编写一个中间件,当它看到某个操作时,会在延迟后打印一些内容

const delayedMessageMiddleware = storeAPI => next => action => {
if (action.type === 'todos/todoAdded') {
setTimeout(() => {
console.log('Added a new todo: ', action.payload)
}, 1000)
}

return next(action)
}

此中间件将查找“todo added”操作。每次看到它时,它都会设置一个 1 秒的计时器,然后将操作的有效负载打印到控制台。

中间件用例

那么,我们可以用中间件做什么?很多事情!

中间件可以在看到调度操作时执行任何它想做的事情

  • 将某些内容记录到控制台
  • 设置超时
  • 进行异步 API 调用
  • 修改操作
  • 暂停操作甚至完全停止操作

以及您可以想到的任何其他事情。

特别是,中间件旨在包含具有副作用的逻辑。此外,中间件可以修改 `dispatch` 以接受不是普通操作对象的任何东西。我们将在第 6 部分:异步逻辑中详细讨论这两点。

Redux DevTools

最后,在配置存储方面,还有一件非常重要的事情需要说明。

Redux 的设计初衷是让您更容易了解您的状态何时、何地、为何以及如何随时间发生变化。作为其中的一部分,Redux 的构建是为了支持使用Redux DevTools - 一个附加组件,它向您展示了调度了哪些操作、这些操作包含了什么以及每个调度操作后状态如何变化的历史记录。

Redux DevTools UI 可作为浏览器扩展程序,适用于ChromeFirefox。如果您还没有将其添加到您的浏览器,请立即添加。

安装完成后,打开浏览器的 DevTools 窗口。您现在应该看到一个新的“Redux”选项卡。它目前还没有任何功能 - 我们需要先将其设置为与 Redux store 通信。

将 DevTools 添加到 Store

安装扩展程序后,我们需要配置 store,以便 DevTools 可以看到 store 内部发生的事情。DevTools 需要添加一个特定的 store enhancer 来实现这一点。

The Redux DevTools Extension 文档 提供了一些关于如何设置 store 的说明,但列出的步骤有点复杂。但是,有一个名为 redux-devtools-extension 的 NPM 包可以处理复杂的部分。该包导出一个专门的 composeWithDevTools 函数,我们可以用它来代替原始的 Redux compose 函数。

以下是它的外观

src/store.js
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'
import { print1, print2, print3 } from './exampleAddons/middleware'

const composedEnhancer = composeWithDevTools(
// EXAMPLE: Add whatever middleware you actually want to use here
applyMiddleware(print1, print2, print3)
// other store enhancers if any
)

const store = createStore(rootReducer, composedEnhancer)
export default store

确保 index.js 在导入 store 后仍然在分派操作。现在,打开浏览器 DevTools 窗口中的 Redux DevTools 选项卡。您应该看到类似于以下内容的内容

Redux DevTools Extension: action tab

左侧有一个分派操作列表。如果我们点击其中一个,右侧窗格会显示几个选项卡

  • 该操作对象的內容
  • reducer 运行后 Redux state 的完整外观
  • 先前 state 与当前 state 之间的差异
  • 如果启用,则会导致最初调用 store.dispatch() 的代码行的函数堆栈跟踪

以下是我们在分派“添加待办事项”操作后“State”和“Diff”选项卡的外观

Redux DevTools Extension: state tab

Redux DevTools Extension: diff tab

这些是非常强大的工具,可以帮助我们调试应用程序并了解内部发生的具体情况。

您学到的知识

如您所见,store 是每个 Redux 应用程序的核心部分。store 包含 state 并通过运行 reducer 处理操作,并且可以自定义以添加其他行为。

让我们看看我们的示例应用程序现在的样子

作为提醒,以下是本节中涵盖的内容

总结
  • Redux 应用始终只有一个 store
    • 使用 Redux 的 createStore API 创建 store
    • 每个 store 都有一个唯一的根 reducer 函数
  • store 有三种主要方法
    • getState 返回当前状态
    • dispatch 将一个 action 发送到 reducer 以更新状态
    • subscribe 接收一个监听回调函数,该函数在每次 dispatch action 时都会运行
  • store 增强器允许我们在创建 store 时对其进行自定义
    • 增强器包装 store 并可以覆盖其方法
    • createStore 接受一个增强器作为参数
    • 可以使用 compose API 将多个增强器合并在一起
  • 中间件是自定义 store 的主要方式
    • 使用 applyMiddleware 增强器添加中间件
    • 中间件被编写为三个相互嵌套的函数
    • 每次 dispatch action 时都会运行中间件
    • 中间件可以在内部具有副作用
  • Redux DevTools 允许您查看应用程序随时间推移的变化
    • 可以在浏览器中安装 DevTools 扩展
    • store 需要使用 composeWithDevTools 添加 DevTools 增强器
    • DevTools 显示了 dispatch 的 action 和状态随时间的变化

下一步?

现在我们拥有一个可以运行 reducer 并根据 dispatch 的 action 更新状态的 Redux store。

但是,每个应用程序都需要一个用户界面来显示数据并让用户执行有用的操作。在 第 5 部分:UI 和 React 中,我们将了解 Redux store 如何与 UI 协同工作,以及 Redux 如何与 React 协同工作。