跳至主内容

Redux 基础教程,第 4 篇:Store

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

你将学习
  • 如何创建 Redux store
  • 如何使用 store 更新状态并监听更新
  • 如何配置 store 以扩展其功能
  • 如何设置 Redux DevTools 扩展来调试应用

简介

第 3 篇:State、Actions 和 Reducers中,我们开始编写示例待办事项应用。我们列出了业务需求,定义了让应用运行所需的 state 结构,并创建了一系列 action 类型来描述"发生了什么",匹配用户与应用交互时可能发生的事件类型。我们还编写了能够处理 state.todosstate.filters 更新的 reducer 函数,并了解了如何使用 Redux 的 combineReducers 函数基于每个功能的"切片 reducer"创建"根 reducer"。

现在是将这些部分整合起来的时候了,我们将介绍 Redux 应用的核心组件:store

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

注意

请注意,本教程有意展示旧式的 Redux 逻辑模式(这些模式比我们当前推荐的 "现代 Redux" 模式需要编写更多代码),目的是为了解释 Redux 背后的原理和概念。它_并非_用于生产环境的项目。

学习使用 Redux Toolkit 实现 "现代 Redux" 模式,请参考以下文档:

Redux Store

Redux store 将构成应用的 state、actions 和 reducers 整合在一起。store 承担着多项职责:

需要重点注意:在 Redux 应用中你只会有一个 store。当需要拆分数据处理逻辑时,应使用 reducer 组合创建多个可组合的 reducer,而非创建多个独立的 store。

创建 Store

每个 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 中移除所有示例待办事项对象,使其变为空数组。这将使本示例的输出更清晰易读。

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 可能在其内部调用其他切片 reducer,例如 todosReducer(state.todos, action)
  • Store 内部保存 新的 状态值

  • Store 调用所有监听器的订阅回调函数

  • 如果监听器能访问 store,现在就可以调用 store.getState() 读取最新状态值

观察示例中的控制台日志输出,可以看到每次派发 action 后 Redux 状态的变化情况:

派发 action 后记录的 Redux 状态

请注意,我们的应用没有记录最后一个 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 返回当前状态值

  • subscribe 维护监听器回调数组,并返回移除新回调的函数

  • dispatch 调用 reducer、保存状态并执行所有监听器

  • Store 在启动时派发一个 action 来初始化 reducer 状态

  • Store API 是包含 {dispatch, subscribe, getState} 的对象

需要特别强调:getState 直接返回当前 state 值。这意味着默认情况下没有任何机制阻止你意外修改当前状态值!以下代码能运行且不报错,但这是错误的:

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

换句话说:

  • 调用 getState() 时,Redux store 不会创建 state 值的额外副本,返回的就是根 reducer 返回的原始引用

  • Redux store 没有其他机制防止意外修改。无论是在 reducer 内部还是 store 外部,都有可能意外改变状态,因此必须时刻警惕避免直接修改。

意外变更的一个常见原因是数组排序操作。调用 array.sort() 实际上会直接变更现有数组。如果我们执行 const sortedTodos = state.todos.sort(),将会无意中变更真实的 store 状态。

技巧

第 8 节:现代 Redux中,我们将看到 Redux Toolkit 如何帮助避免 reducer 中的变更,并检测和警告 reducer 外部的意外变更。

配置 Store

我们已经知道可以向 createStore 传递 rootReducerpreloadedState 参数。但 createStore 还可以接受第三个参数,用于定制 store 的能力并赋予其新功能。

Redux store 通过 store enhancer(store 增强器)进行定制。store enhancer 就像是 createStore 的特殊版本,它在原始 Redux store 外部添加了包装层。增强后的 store 可以改变其行为方式,通过提供自定义的 dispatchgetStatesubscribe 函数实现功能扩展。

本教程不深入探讨 store enhancer 的内部工作机制,我们将重点放在实际使用上。

创建带 Enhancer 的 Store

项目中提供了两个简单的 store enhancer 示例,位于 src/exampleAddons/enhancers.js 文件:

  • sayHiOnDispatch:每次 dispatch action 时在控制台打印 'Hi'!

  • includeMeaningOfLife:在 getState() 返回值中添加 meaningOfLife: 42 字段

让我们从使用 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

接下来尝试 dispatch 一个 action:

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 enhancer 用自己的 dispatch 函数包装了原始的 store.dispatch。当我们调用 store.dispatch() 时,实际调用的是来自 sayHiOnDispatch 的包装函数,它会先调用原始函数再打印 'Hi'。

现在尝试添加第二个 enhancer。我们可以从同一文件导入 includeMeaningOfLife... 但这里有个问题:createStore 只接受一个 enhancer 作为第三个参数! 如何同时传递 两个 enhancer?

我们真正需要的是将 sayHiOnDispatchincludeMeaningOfLife 合并为单个 enhancer 的方法。

幸运的是,Redux 核心库包含 compose 函数,可用于合并多个 enhancer

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

现在看看使用这个 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

可以看到两个 enhancer 同时修改了 store 的行为:sayHiOnDispatch 改变了 dispatch 的工作方式,而 includeMeaningOfLife 改变了 getState 的工作方式。

Store 增强器是一种极其强大的修改 store 的方式,几乎所有 Redux 应用在配置 store 时都会至少包含一个增强器。

技巧

如果不需要传入任何 preloadedState,可以将 enhancer 直接作为第二个参数传入:

const store = createStore(rootReducer, storeEnhancer)

中间件

增强器的强大之处在于它们能覆盖或替换 store 的任何方法:dispatchgetStatesubscribe

但多数情况下,我们只需要定制 dispatch 的行为。如果能有一种方式在 dispatch 执行时添加自定义行为就非常理想了。

Redux 使用一种特殊的插件 中间件 来定制 dispatch 函数。

如果您使用过 Express 或 Koa 这类框架,可能已经熟悉通过添加中间件来定制行为的理念。在这些框架中,中间件是放置在框架接收请求和生成响应之间的代码。例如 Express 或 Koa 的中间件可以添加 CORS 头信息、日志记录、压缩等功能。中间件最出色的特性是它们可以链式组合,您可以在单个项目中组合使用多个独立的第三方中间件。

Redux 中间件解决的问题与 Express/Koa 不同,但概念上异曲同工。Redux 中间件在派发 action 和 action 到达 reducer 之间提供了第三方扩展点。开发者常用 Redux 中间件实现日志记录、错误监控、异步 API 通信、路由跳转等功能。

我们先学习如何向 store 添加中间件,然后再讲解如何编写自定义中间件。

使用中间件

我们已经知道可以通过 store 增强器定制 Redux store。实际上 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

正如其名称所示,这些中间件会在 action 被派发时打印数字。

现在如果派发 action 会怎样?

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

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

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

中间件日志打印

那么这是如何实现的呢?

中间件围绕 store 的 dispatch 方法形成处理管道。当我们调用 store.dispatch(action) 时,实际上是在调用管道中的第一个中间件。中间件在处理 action 时可以执行任何操作。通常,中间件会检查 action 是否是其关注的特定类型(类似 reducer 的处理方式)。如果是目标类型,中间件可能执行自定义逻辑;否则将 action 传递给管道中的下一个中间件。

reducer 不同,中间件内部可以包含副作用,包括定时器和异步逻辑。

在此案例中,action 的传递路径是:

  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 调用,并接收一个包含 store 的 {dispatch, getState} 函数的 storeAPI 对象。这些 dispatchgetState 函数与 store 中的实际函数相同。如果调用这个 dispatch 函数,它会将 action 发送到中间件管道的_起点_。该函数仅被调用一次。

  • wrapDispatch:中间层函数接收一个名为 next 的函数作为参数。该函数实际上是管道中的_下一个中间件_。如果当前中间件是序列中的最后一个,那么 next 就是原始的 store.dispatch 函数。调用 next(action) 会将 action 传递给管道中的_下一个_中间件。该函数同样仅被调用一次。

  • handleAction:最内层函数接收当前的 action 作为参数,并且_每次_ dispatch action 时都会被调用。

技巧

你可以随意命名这些中间件函数,但使用以下名称有助于记忆各层功能:

  • 外层:someCustomMiddleware(或你自定义的中间件名称)
  • 中间层:wrapDispatch
  • 内层:handleAction

由于这些都是普通函数,我们也可以使用 ES2015 箭头函数来编写。箭头函数可以省略 return 语句,从而缩短代码,但如果你还不熟悉箭头函数和隐式返回,可能会稍微降低可读性。

以下是用箭头函数重写的相同示例:

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

return next(action)
}

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

编写你的第一个自定义中间件

假设我们想在应用中添加日志功能。我们希望在每个 action 被 dispatch 时在控制台查看其内容,并希望看到 action 经过 reducer 处理后的状态。

信息

这些示例中间件并非实际待办事项应用的组成部分,但你可以尝试将其添加到项目中,观察使用时的效果。

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

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

每当一个 action 被 dispatch 时:

  • 首先执行 handleAction 函数的第一部分,我们打印 'dispatching'

  • 将 action 传递给 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!'

我们再试一个例子。中间件通常会监听特定 action,并在其被 dispatch 时执行某些操作。中间件还具备运行异步逻辑的能力。我们可以编写一个中间件,在识别到特定 action 后延迟打印内容:

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" action。每当识别到该 action,它会设置一个 1 秒的定时器,然后将 action 的 payload 打印到控制台。

中间件的使用场景

那么,中间件能做什么呢?非常多的事情!

中间件在看到一个被派发的 action 时,可以执行任何操作:

  • 在控制台记录日志

  • 设置超时

  • 发起异步 API 调用

  • 修改 action

  • 暂停 action 甚至完全停止它

以及任何你能想到的事情。

特别地,中间件旨在包含具有副作用的逻辑。此外,中间件可以修改 dispatch 以接受非普通 action 对象的内容。我们将在第六节:异步逻辑中详细讨论这两点。

Redux DevTools 开发者工具

最后,在配置 store 时还有一件非常重要的事情需要介绍。

Redux 是专门设计来让你更容易理解状态随时间变化的时间、地点、原因和方式的。作为其中的一部分,Redux 被构建成支持使用 Redux DevTools —— 一个插件,用于展示已派发的 action 历史记录、这些 action 包含的内容以及每次派发 action 后状态如何变化。

Redux DevTools 的用户界面可作为浏览器扩展在 ChromeFirefox 上使用。如果你尚未将其添加到浏览器中,请立即添加。

安装完成后,打开浏览器的开发者工具窗口。你现在应该能看到一个新的 "Redux" 标签页。但它目前还不能做任何事情 —— 我们需要先将其设置为与 Redux store 进行通信。

将开发者工具添加到 Store

扩展安装后,我们需要配置 store 以便开发者工具能够看到内部发生的情况。开发者工具需要添加一个特定的 store 增强器(store enhancer)才能实现这一点。

Redux DevTools 扩展文档中有一些关于如何设置 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 后仍然派发了一个 action。现在,打开浏览器开发者工具中的 Redux DevTools 标签页。你应该会看到类似这样的内容:

Redux DevTools 扩展:action 标签页

左侧是已派发 action 的列表。如果我们点击其中一个,右侧面板会显示几个标签页:

  • 该 action 对象的内容

  • reducer 运行后的整个 Redux 状态

  • 前一状态与此状态之间的差异

  • 如果启用,还会显示函数调用栈,回溯到最初调用 store.dispatch() 的代码行

以下是我们派发 "add todo" action 后 "State" 和 "Diff" 标签页的样子:

Redux DevTools 扩展:state 标签页

Redux DevTools 扩展:diff 标签页

这些都是非常强大的工具,可以帮助我们调试应用并准确理解内部发生的情况。

学习要点

如你所见,store 是每个 Redux 应用的核心组件。它包含状态并通过运行 reducer 处理 action,还能通过定制添加额外行为。

现在让我们看看示例应用的实现效果:

再次提醒,本节涵盖的核心内容如下:

总结
  • Redux 应用永远只有单一 store
    • 使用 Redux createStore API 创建 store
    • 每个 store 都有唯一的根 reducer 函数
  • store 包含三个核心方法
    • getState 返回当前状态
    • dispatch 发送 action 到 reducer 来更新状态
    • subscribe 注册在每次 action 派发后执行的监听回调
  • store enhancer 支持创建时定制 store
    • enhancer 包装 store 并可覆盖其方法
    • createStore 接受单个 enhancer 参数
    • 多个 enhancer 可通过 compose API 合并
  • 中间件是定制 store 的主要方式
    • 使用 applyMiddleware enhancer 添加中间件
    • 中间件由三层嵌套函数构成
    • 每次 action 派发时中间件都会执行
    • 中间件内部可包含副作用逻辑
  • Redux DevTools 可追踪应用状态变化
    • DevTools 扩展程序可安装在浏览器中
    • 需通过 composeWithDevTools 添加 DevTools enhancer
    • DevTools 显示历史派发的 action 及状态变更轨迹

下一步是什么?

现在我们已经拥有可运行的 Redux store,它能在派发 action 时执行 reducer 并更新状态。

然而每个应用都需要用户界面来展示数据并提供交互功能。在第五部分:UI 与 React中,我们将探索 Redux store 如何与 UI 协同工作,特别是如何与 React 集成使用。