Redux 基础教程,第 4 篇:Store
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
- 如何创建 Redux store
- 如何使用 store 更新状态并监听更新
- 如何配置 store 以扩展其功能
- 如何设置 Redux DevTools 扩展来调试应用
简介
在第 3 篇:State、Actions 和 Reducers中,我们开始编写示例待办事项应用。我们列出了业务需求,定义了让应用运行所需的 state 结构,并创建了一系列 action 类型来描述"发生了什么",匹配用户与应用交互时可能发生的事件类型。我们还编写了能够处理 state.todos 和 state.filters 更新的 reducer 函数,并了解了如何使用 Redux 的 combineReducers 函数基于每个功能的"切片 reducer"创建"根 reducer"。
现在是将这些部分整合起来的时候了,我们将介绍 Redux 应用的核心组件:store。
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
请注意,本教程有意展示旧式的 Redux 逻辑模式(这些模式比我们当前推荐的 "现代 Redux" 模式需要编写更多代码),目的是为了解释 Redux 背后的原理和概念。它_并非_用于生产环境的项目。
学习使用 Redux Toolkit 实现 "现代 Redux" 模式,请参考以下文档:
- 完整的 "Redux 必备教程":使用 Redux Toolkit 教授"如何以正确方式使用 Redux"构建真实应用。我们推荐所有 Redux 学习者阅读此教程!
- Redux 基础教程,第 8 部分:使用 Redux Toolkit 的现代 Redux:展示如何将前面章节的底层示例转换为现代 Redux Toolkit 实现
Redux Store
Redux store 将构成应用的 state、actions 和 reducers 整合在一起。store 承担着多项职责:
-
内部持有当前应用状态
-
通过
store.getState()允许访问当前状态 -
通过
store.dispatch(action)允许更新状态 -
通过
store.subscribe(listener)注册监听器回调 -
通过
store.subscribe(listener)返回的unsubscribe函数处理监听器的注销
需要重点注意:在 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:
import { createStore } from 'redux'
import rootReducer from './reducer'
const store = createStore(rootReducer)
export default store
加载初始状态
createStore 还可以接受 preloadedState 值作为第二个参数。你可以用它来在创建 store 时添加初始数据,例如包含在服务器发送的 HTML 页面中的值,或从 localStorage 持久化读取并在用户再次访问页面时恢复的值,如下所示:
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 中移除所有示例待办事项对象,使其变为空数组。这将使本示例的输出更清晰易读。
// 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)
- 该根 reducer 可能在其内部调用其他切片 reducer,例如
-
Store 内部保存 新的 状态值
-
Store 调用所有监听器的订阅回调函数
-
如果监听器能访问
store,现在就可以调用store.getState()读取最新状态值
观察示例中的控制台日志输出,可以看到每次派发 action 后 Redux 状态的变化情况:

请注意,我们的应用没有记录最后一个 action 的任何内容。这是因为我们调用 unsubscribe() 时移除了监听器回调,因此在 action 派发后不再执行任何操作。
在编写 UI 前我们就已定义了应用的行为逻辑,这有助于 确保应用按预期运行。
如果你愿意,现在可以尝试为 reducer 编写测试。由于它们是纯函数,测试应该很直接:传入示例 state 和 action,
检查返回结果是否符合预期:
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 精简实现示例:
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 传递 rootReducer 和 preloadedState 参数。但 createStore 还可以接受第三个参数,用于定制 store 的能力并赋予其新功能。
Redux store 通过 store enhancer(store 增强器)进行定制。store enhancer 就像是 createStore 的特殊版本,它在原始 Redux store 外部添加了包装层。增强后的 store 可以改变其行为方式,通过提供自定义的 dispatch、getState 和 subscribe 函数实现功能扩展。
本教程不深入探讨 store enhancer 的内部工作机制,我们将重点放在实际使用上。
创建带 Enhancer 的 Store
项目中提供了两个简单的 store enhancer 示例,位于 src/exampleAddons/enhancers.js 文件:
-
sayHiOnDispatch:每次 dispatch action 时在控制台打印'Hi'! -
includeMeaningOfLife:在getState()返回值中添加meaningOfLife: 42字段
让我们从使用 sayHiOnDispatch 开始。首先导入它,然后传递给 createStore:
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:
import store from './store'
console.log('Dispatching action')
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
console.log('Dispatch complete')
查看控制台,你应该会在其他两条日志之间看到打印的 'Hi!':
sayHiOnDispatch enhancer 用自己的 dispatch 函数包装了原始的 store.dispatch。当我们调用 store.dispatch() 时,实际调用的是来自 sayHiOnDispatch 的包装函数,它会先调用原始函数再打印 'Hi'。
现在尝试添加第二个 enhancer。我们可以从同一文件导入 includeMeaningOfLife... 但这里有个问题:createStore 只接受一个 enhancer 作为第三个参数! 如何同时传递 两个 enhancer?
我们真正需要的是将 sayHiOnDispatch 和 includeMeaningOfLife 合并为单个 enhancer 的方法。
幸运的是,Redux 核心库包含 compose 函数,可用于合并多个 enhancer:
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 会发生什么:
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}
日志输出如下:

可以看到两个 enhancer 同时修改了 store 的行为:sayHiOnDispatch 改变了 dispatch 的工作方式,而 includeMeaningOfLife 改变了 getState 的工作方式。
Store 增强器是一种极其强大的修改 store 的方式,几乎所有 Redux 应用在配置 store 时都会至少包含一个增强器。
如果不需要传入任何 preloadedState,可以将 enhancer 直接作为第二个参数传入:
const store = createStore(rootReducer, storeEnhancer)
中间件
增强器的强大之处在于它们能覆盖或替换 store 的任何方法:dispatch、getState 和 subscribe。
但多数情况下,我们只需要定制 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 开始,并添加本项目包含的三个示例中间件。
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 会怎样?
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 的传递路径是:
-
print1中间件(我们看到的是store.dispatch) -
print2中间件 -
print3中间件 -
原始的
store.dispatch方法 -
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对象。这些dispatch和getState函数与 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 的用户界面可作为浏览器扩展在 Chrome 和 Firefox 上使用。如果你尚未将其添加到浏览器中,请立即添加。
安装完成后,打开浏览器的开发者工具窗口。你现在应该能看到一个新的 "Redux" 标签页。但它目前还不能做任何事情 —— 我们需要先将其设置为与 Redux store 进行通信。
将开发者工具添加到 Store
扩展安装后,我们需要配置 store 以便开发者工具能够看到内部发生的情况。开发者工具需要添加一个特定的 store 增强器(store enhancer)才能实现这一点。
Redux DevTools 扩展文档中有一些关于如何设置 store 的说明,但其中列出的步骤有点复杂。不过,有一个名为 redux-devtools-extension 的 NPM 包可以处理复杂的部分。该包导出了一个专门的 composeWithDevTools 函数,我们可以用它来替代 Redux 原生的 compose 函数。
具体用法如下:
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 标签页。你应该会看到类似这样的内容:

左侧是已派发 action 的列表。如果我们点击其中一个,右侧面板会显示几个标签页:
-
该 action 对象的内容
-
reducer 运行后的整个 Redux 状态
-
前一状态与此状态之间的差异
-
如果启用,还会显示函数调用栈,回溯到最初调用
store.dispatch()的代码行
以下是我们派发 "add todo" action 后 "State" 和 "Diff" 标签页的样子:


这些都是非常强大的工具,可以帮助我们调试应用并准确理解内部发生的情况。
学习要点
如你所见,store 是每个 Redux 应用的核心组件。它包含状态并通过运行 reducer 处理 action,还能通过定制添加额外行为。
现在让我们看看示例应用的实现效果:
再次提醒,本节涵盖的核心内容如下:
- Redux 应用永远只有单一 store
- 使用 Redux
createStoreAPI 创建 store - 每个 store 都有唯一的根 reducer 函数
- 使用 Redux
- store 包含三个核心方法
getState返回当前状态dispatch发送 action 到 reducer 来更新状态subscribe注册在每次 action 派发后执行的监听回调
- store enhancer 支持创建时定制 store
- enhancer 包装 store 并可覆盖其方法
createStore接受单个 enhancer 参数- 多个 enhancer 可通过
composeAPI 合并
- 中间件是定制 store 的主要方式
- 使用
applyMiddlewareenhancer 添加中间件 - 中间件由三层嵌套函数构成
- 每次 action 派发时中间件都会执行
- 中间件内部可包含副作用逻辑
- 使用
- Redux DevTools 可追踪应用状态变化
- DevTools 扩展程序可安装在浏览器中
- 需通过
composeWithDevTools添加 DevTools enhancer - DevTools 显示历史派发的 action 及状态变更轨迹
下一步是什么?
现在我们已经拥有可运行的 Redux store,它能在派发 action 时执行 reducer 并更新状态。
然而每个应用都需要用户界面来展示数据并提供交互功能。在第五部分:UI 与 React中,我们将探索 Redux store 如何与 UI 协同工作,特别是如何与 React 集成使用。