Redux 基础,第 4 部分:存储(Store)
- 如何创建 Redux store
- 如何使用 store 来更新状态并监听状态更新
- 如何配置 store 扩展其功能
- 如何设置 Redux DevTools 扩展以调试你的应用
介绍
在第3部分:状态、动作和 reducers中,我们开始编写示例待办事项应用。我们列出了业务需求,定义了使应用正常运行所需的状态结构,并创建了一系列动作类型来描述“发生了什么”,以匹配用户与应用交互时可能发生的事件。我们还编写了可以更新 state.todos 和 state.filters 部分的reducer函数,并看到了如何使用 Redux 的 combineReducers 函数来基于应用中每个功能的不同“切片 reducer”创建“根 reducer”。
现在,是时候将这些部分组合起来,Redux 应用的核心部分:store(存储)。
请注意,本教程刻意展示了较旧的 Redux 逻辑模式,这些模式需要比我们现在推荐的使用 Redux Toolkit 的“现代 Redux”模式更多的代码,目的是为了讲解 Redux 背后的原理和概念。它并非一个可用于生产环境的项目。
请参考以下页面,学习如何使用带有 Redux Toolkit 的“现代 Redux”:
- 完整的“Redux Essentials”教程,讲解了如何使用 Redux Toolkit 以正确的方式构建真实世界应用。我们建议所有 Redux 学习者都应该阅读这份“Essentials”教程!
- Redux 基础,第 8 部分:使用 Redux Toolkit 的现代 Redux,展示了如何将早期章节中的底层示例转换为现代 Redux Toolkit 对应实现
Redux Store(存储)
Redux store 将组成你应用的状态、动作和 reducers 结合起来。store 有几个职责:
- Holds the current application state inside
- Allows access to the current state via
store.getState(); - Allows state to be updated via
store.dispatch(action); - Registers listener callbacks via
store.subscribe(listener); - Handles unregistering of listeners via the
unsubscribefunction returned bystore.subscribe(listener).
需要注意的是,在 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,用于添加初始数据,比如从服务器发送的 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)
派发动作(Dispatching Actions)
现在我们创建了 store,来验证程序是否正常工作!即使没有 UI,我们也能测试更新逻辑。
在运行以下代码前,试着回到 src/features/todos/todosSlice.js,将 initialState 中所有示例待办事项对象删除,使其成为一个空数组。这样示例的输出会更易读。
// Omit existing React imports
import store from './store'
// 打印初始状态
console.log('Initial state: ', store.getState())
// {todos: [....], filters: {status, colors}}
// 每次状态改变时打印状态
// 注意 subscribe() 返回一个用来注销监听的函数
const unsubscribe = store.subscribe(() =>
console.log('State after dispatch: ', store.getState())
)
// 现在,派发一些动作
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' }
})
// 停止监听状态更新
unsubscribe()
// 派发最后一个动作看看结果
store.dispatch({ type: 'todos/todoAdded', payload: 'Try creating a store' })
// 省略已有的 React 渲染逻辑
请记住,每次调用 store.dispatch(action) 时:
- store 会调用
rootReducer(state, action)- 根 reducer 可能会调用其内部其他切片 reducer,如
todosReducer(state.todos, action)
- 根 reducer 可能会调用其内部其他切片 reducer,如
- store 保存新的状态值
- store 调用所有监听订阅回调函数
- 如果监听器访问了
store,就可以调用store.getState()读取最新状态
从上面示例的控制台输出,可以看到每个动作派发后,Redux 状态是如何变化的:

注意我们的应用并没有记录最后一个动作的任何日志,因为我们调用了 unsubscribe() 取消了监听器,因此该动作派发后没有任何监听回调运行。
我们在开始写 UI 前就指定了应用的行为,这有助于我们确信应用会按预期工作。
你也可以尝试编写 reducer 的测试用例。由于 reducer 是纯函数,测试非常直接。调用 reducer,传入示例 state 和 action,并检查结果是否符合预期:
import todosReducer from './todosSlice'
test('根据 id 切换待办状态', () => {
const initialState = [{ id: 0, 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 初始化时派发一次动作,初始化状态
- store API 是一个包含
{dispatch, subscribe, getState}的对象
特别强调一点:注意 getState 只是返回当前 state 的引用。也就是说 默认情况下,没有机制防止你意外修改状态! 下面的代码虽然能运行,但不正确:
const state = store.getState()
// ❌ 不要这样做 — 它会修改当前状态!
state.filters.status = 'Active'
换句话说:
- Redux store 调用
getState()返回的状态是根 reducer 返回的相同引用,没有额外拷贝 - Redux store 不会阻止你意外修改状态。状态既可以在 reducer 内部修改,也可以在 store 外部修改,你必须小心避免这种情况
一个常见引发意外修改的例子是对数组排序。调用 array.sort() 实际是修改了原数组。如果执行 const sortedTodos = state.todos.sort(),那么就会无意中修改真实的 store 状态。
在第 8 部分:现代 Redux中,我们会看到 Redux Toolkit 如何帮助避免在 reducer 内的状态修改,并检测和警告 reducer 外的意外修改。
配置 Store
我们已经看到传入 rootReducer 和 preloadedState 可以创建 store。但是 createStore 还接受一个额外参数,用于定制 store 的功能并赋予它新能力。
Redux store 通过**store enhancer(增强器)**定制。store enhancer 就是一个特殊版本的 createStore,它包裹住原 Redux store,对其行为进行改变,为 store 的 dispatch、getState 和 subscribe 方法提供自定义版本。
本教程不深入增强器的内部工作原理,而专注于如何使用它们。
使用增强器创建 Store
我们项目中有两个示例 store enhancer,位于 src/exampleAddons/enhancers.js:
sayHiOnDispatch:每当派发动作时,都会在控制台打印'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。
接着派发一个动作了解效果:
import store from './store'
console.log('Dispatching action')
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
console.log('Dispatch complete')
看看控制台,你会发现 'Hi!' 被打印在两条日志语句之间:
sayHiOnDispatch 用自己的 dispatch 函数包装了原始的 store.dispatch。当我们调用 store.dispatch(),实际执行的是这个包装函数,它调用原始 dispatch 后再打印“Hi”。
接下来试着添加第二个 enhancer。我们可以同时导入 includeMeaningOfLife,但问题来了。createStore 作为第三个参数只接受一个 enhancer! 如何同时传入两个 enhancer 呢?
此时我们需要一个合并多个 enhancer 的方法,生成一个组合 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
然后运行看看:
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 enhancer 是非常强大的,可以深度定制 store,大多数 Redux 应用设置 store 时都会用到至少一个 enhancer。
如果没有传入 preloadedState,也可以把 enhancer 作为第二参数传入:
const store = createStore(rootReducer, storeEnhancer)
中间件(Middleware)
增强器很强大,因为它们能覆盖或替换 store 的任意方法:dispatch、getState 和 subscribe。
不过很多时候,我们只想自定义 dispatch 的行为,希望在 dispatch 被调用时添加自定义逻辑。
Redux 使用一种特殊的插件称为**中间件(middleware)**来实现对 dispatch 的自定义。
如果你用过 Express 或 Koa 之类的库,可能已经熟悉中间件概念。这些框架中,中间件是放在请求和响应之间的代码,比如添加 CORS 头部、日志、压缩等。中间件的最大特性是它们可以串联组合,在一个项目中同时使用多个独立中间件。
Redux 中间件解决的是不同的问题,但理念类似。Redux 中间件提供了一个第三方扩展点,介于派发动作和动作到达 reducer 之间。 中间件可用于日志记录、崩溃报告、异步 API 调用、路由等。
先来看如何把中间件加入 store,再见识如何编写自定义中间件。
使用中间件
我们已经知道通过 store enhancer 可以自定义 store。Redux 中间件是基于 Redux 自带的一个特殊 store enhancer——applyMiddleware 实现的。
因为我们会用增强器,所以也可以使用中间件。以下示例使用了三个项目中包含的示例中间件:
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducer'
import { print1, print2, print3 } from './exampleAddons/middleware'
const middlewareEnhancer = applyMiddleware(print1, print2, print3)
// 没有 preloadedState,第二参数传 enhancer
const store = createStore(rootReducer, middlewareEnhancer)
export default store
这些中间件就是分别打印数字的函数。
派发动作时会怎样?
import store from './store'
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: '1'
// log: '2'
// log: '3'
控制台输出:
这是怎么做到的?
中间件是包裹在 store 的 dispatch 方法外层的一条管线。调用 store.dispatch(action),实际执行的是管线中的第一个中间件。它可以对动作做任何处理。通常,中间件会检测动作类型,如果是它感兴趣的类型,就执行自定义逻辑,否则将动作传给下一个中间件。
与 reducer 不同的是,中间件允许执行副作用代码,包括定时器和其它异步逻辑。
此例中,动作依次经过:
print1中间件(即store.dispatch)print2中间件print3中间件- 原始的
store.dispatch - store 中的根 reducer
因为这些都是函数调用,所以调用栈会依次返回。因此 print1 是第一个运行的,也是最后完成的。
编写自定义中间件
你也可以编写自己的中间件。虽然没必要一直写自定义中间件,但中间件是向 Redux 应用添加特定行为的绝佳方式。
Redux 中间件形态是一组嵌套的三层函数。来看用 function 关键字写的例子,方便理解:
// 使用 ES5 函数写的中间件
// 外层函数:
function exampleMiddleware(storeAPI) {
return function wrapDispatch(next) {
return function handleAction(action) {
// 这里可以传递动作给 next(action),
// 或调用 storeAPI.dispatch(action) 从管线起点重新开始,
// 还可以使用 storeAPI.getState()
return next(action)
}
}
}
解释这三层函数及参数:
exampleMiddleware:外层函数即中间件本体,会被applyMiddleware调用,接收storeAPI,包含{dispatch, getState}。调用这个dispatch将动作送入中间件管线起点。此函数只调用一次。wrapDispatch:中间层函数参数是next,表示管线中的下一个中间件函数。如果当前是最后一个中间件,则next是原始的store.dispatch。调用next(action)把动作传给管线下一个。此函数也只调用一次。handleAction:内层函数接收当前action,每次派发动作时都会调用。
你可以给这三个函数起任意名字,但使用下面名字有助理解:
- 外层:
someCustomMiddleware(你的中间件名) - 中层:
wrapDispatch - 内层:
handleAction
用箭头函数也能写得更短,利用隐式返回:
const anotherExampleMiddleware = storeAPI => next => action => {
// 每次动作派发时这里执行
return next(action)
}
依然是三层嵌套并返回函数,只是代码更简洁。
你的第一个自定义中间件
假如我们想给应用添加日志,打印每个动作和动作后状态:
这些示例中间件不属于实际 todo 应用,但你可以添加进项目看看效果。
示例中间件:
const loggerMiddleware = storeAPI => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', storeAPI.getState())
return result
}
动作被派发时:
handleAction的第一条语句打印 'dispatching'- 调用
next(action)传给下一个中间件或真实的store.dispatch - reducer 运行,状态更新,
next返回 - 调用
storeAPI.getState()查看新状态并打印 - 最后返回执行结果
任何中间件都能够返回任何值,而管线第一个中间件的返回值会成为调用 store.dispatch() 的返回结果。例如:
const alwaysReturnHelloMiddleware = storeAPI => next => action => {
const originalResult = next(action)
// 忽略原始结果,返回其它值
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)
}
此中间件监听“待办添加”动作,每次见到时设置 1 秒定时器,然后打印动作的有效载荷。
中间件的实际应用
中间件能对派发的动作做任何事:
- 打印日志
- 设置定时器
- 发起异步 API 调用
- 修改动作
- 暂停甚至阻止动作传递
以及你能想到的其他操作。
特别是,中间件旨在执行副作用逻辑。此外,中间件还可以修改 dispatch,使它接受非普通动作对象。这部分内容我们将在第 6 部分:异步逻辑中详细讲解。
Redux DevTools
最后,还有一件重要的事要讲:配置 store。
Redux 的设计初衷之一是方便理解状态随时间如何被改变。因此,Redux 支持使用 Redux DevTools —— 一个展示动作历史、动作内容和状态变化的调试插件。
Redux DevTools UI 作为浏览器扩展提供,下载地址为 Chrome:https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en 以及 Firefox:https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/。如果还没安装,赶紧安装一下。
安装后打开浏览器开发者工具,你会看到多了个 “Redux” 标签。但它现在还没作用,需要先配置 store 让它能通信。
在 Store 中添加 DevTools 支持
安装扩展后,需要配置 store,添加 DevTools 增强器。官方文档(Redux DevTools Extension docs)中配置方式略显复杂,但有个 NPM 包 redux-devtools-extension 能简化配置,它导出一个 composeWithDevTools 函数,替代 Redux 原来的 compose。
Redux DevTools 扩展文档介绍了如何配置 store,但其中步骤有些复杂。NPM 上的 @redux-devtools/extension 包可以替你处理复杂部分。它导出了专用的 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:这里添加你想用的中间件
applyMiddleware(print1, print2, print3)
// 其它 store 增强器,如果有的话
)
const store = createStore(rootReducer, composedEnhancer)
export default store
确认 main.jsx 在导入 store 后仍会派发一个 action。然后打开浏览器 DevTools 窗口中的 Redux DevTools 标签页,应该会看到类似如下内容:

左侧是派发动作列表。点击某个动作,右侧显示多个标签:
- 动作对象内容
- 运行 reducer 后完整状态
- 状态差异(diff)
- 开启时,调用
store.dispatch()的代码堆栈跟踪
以下展示“State”和“Diff”标签页,执行 “add todo” 动作后:


这些都是非常强大的工具,帮助我们调试应用,准确理解内部发生了什么。
你学到了什么
如你所见,store 是 Redux 应用的核心部分。store 包含状态,通过 reducer 处理动作,并且能够被定制以扩展额外功能。
来看我们示例应用此刻的样貌:
再来回顾本节内容:
- Redux 应用始终只有一个 store
- 使用 Redux 的
createStoreAPI 创建 store - 每个 store 有一个根 reducer 函数
- 使用 Redux 的
- store 有三个主要方法
getState返回当前状态dispatch派发动作给 reducer 更新状态subscribe注册监听器,每次动作派发时被调用
- store enhancer 用于定制 store
- enhancer 包裹 store,可以覆盖 store 方法
createStore只接受一个 enhancer 参数- 多个 enhancer 可以用
composeAPI 合并
- 中间件是定制 store 的主要方式
- 使用
applyMiddlewareenhancer 添加中间件 - 中间件由三层嵌套函数组成
- 每次动作派发时都会执行中间件
- 中间件中可以执行副作用
- 使用
- Redux DevTools 让你看到应用状态随时间变化
- 在浏览器安装 DevTools 扩展
- store 需要用
composeWithDevTools添加 DevTools enhancer - DevTools 显示派发动作和状态变化
接下来?
我们现在有了一个能运行 reducer、并响应动作更新状态的 store。
然而,每个应用都需要 UI 来展示数据并让用户交互。在第5部分:UI 和 React中,我们将了解 Redux store 如何和 UI 配合,特别是在 React 中的应用。