Redux入门笔记
最近开始看一些关于react的知识,不可避免的接触到了redux。
去看了看他的文档,就来简单总结下。
注意,原大部分内容已经过时了,因为react更新了hook,我接下来回去研究下react-redux针对hook做了哪些改进,再来重新解释,不过本博客大部分内容是讲的原生的redux,还可以放心食用。
Redux是什么
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。 (如果你需要一个 WordPress 框架,请查看 Redux Framework。)
可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。不仅于此,它还提供 超爽的开发体验,比如有一个时间旅行调试器可以编辑后实时预览。
Redux 除了和 React 一起用外,还支持其它界面库。 它体小精悍(只有2kB,包括依赖)。
Redux简述
应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。 惟一改变 state 的办法是触发 action,一个描述发生什么的对象。 为了描述 action 如何改变 state 树,你需要编写 reducers。
就是这样!
1 | import { createStore } from 'redux'; |
你应该把要做的修改变成一个普通对象,这个对象被叫做 action,而不是直接修改 state。然后编写专门的函数来决定每个 action 如何改变应用的 state,这个函数被叫做 reducer。
核心概念
Redux 本身很简单。
当使用普通对象来描述应用的 state 时。例如,todo 应用的 state 可能长这样:
1 | { |
这个对象就像 “Model”,区别是它并没有 setter(修改器方法)。因此其它的代码不能随意修改它,造成难以复现的 bug。
要想更新 state 中的数据,你需要发起一个 action。Action 就是一个普通 JavaScript 对象(注意到没,这儿没有任何魔法)用来描述发生了什么。下面是一些 action 的示例:
1 | { type: 'ADD_TODO', text: 'Go to swimming pool' } |
强制使用 action 来描述所有变化带来的好处是可以清晰地知道应用中到底发生了什么。如果一些东西改变了,就可以知道为什么变。action 就像是描述发生了什么的指示器。最终,为了把 action 和 state 串起来,开发一些函数,这就是 reducer。
再次地,没有任何魔法,reducer 只是一个接收 state 和 action,并返回新的 state 的函数。 对于大的应用来说,不大可能仅仅只写一个这样的函数,所以我们编写很多小函数来分别管理 state 的一部分:
1 | function visibilityFilter(state = 'SHOW_ALL', action) { |
再开发一个 reducer 调用这两个 reducer,进而来管理整个应用的 state:
1 | function todoApp(state = {}, action) { |
三大原则
- 单一数据源,即只有一个state
- state是只读的,你不能直接对state赋值,唯一能够修改state的方法就是触发action
- 使用纯函数来执行修改,当触发了一个action,具体执行的reducer需要时一个纯函数,也就是不能直接对state进行修改,而是要返回一个新的state对象。
基础
Action
首先,让我们来给 action 下个定义。
Action 是把数据从应用(译者注:这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch()
将 action 传到 store。
添加新 todo 任务的 action 是这样的:
1 | const ADD_TODO = 'ADD_TODO' |
Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type
字段来表示将要执行的动作。多数情况下,type
会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。
Action 创建函数
Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。
在 Redux 中的 action 创建函数只是简单的返回一个 action:
1 | function addTodo(text) { |
这样做将使 action 创建函数更容易被移植和测试。
Redux 中只需把 action 创建函数的结果传给 dispatch()
方法即可发起一次 dispatch 过程。
1 | dispatch(addTodo(text)) |
或者创建一个 被绑定的 action 创建函数 来自动 dispatch:
1 | const boundAddTodo = text => dispatch(addTodo(text)) |
然后直接调用它们:
1 | boundAddTodo(text); |
Reducer
Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。
Action 处理
现在我们已经确定了 state 对象的结构,就可以开始开发 reducer。reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。
1 | (previousState, action) => newState |
之所以将这样的函数称之为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue)
里的回调函数属于相同的类型。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:
- 修改传入参数;
- 执行有副作用的操作,如 API 请求和路由跳转;
- 调用非纯函数,如
Date.now()
或Math.random()
。
谨记 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
Reducer的拆分与合成
随着状态越来越复杂,我们的reducer必定会越来越带,所以我们就要想办法把reducer拆分,最后再像办法再合起来,因为我们只能由一个state。
针对我们一开始核心概念那一节的例子,Redux 提供了 combineReducers()
工具类
1 | import { combineReducers } from 'redux' |
注意上面的写法和下面完全等价:
1 | export default function todoApp(state = {}, action) { |
你也可以给它们设置不同的 key,或者调用不同的函数。下面两种合成 reducer 方法完全等价:
1 | const reducer = combineReducers({ |
combineReducers()
所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。
Store
Store 就是把它们联系到一起的对象。Store 有以下职责:
- 维持应用的 state;
- 提供
getState()
方法获取 state; - 提供
dispatch(action)
方法更新 state; - 通过
subscribe(listener)
注册监听器; - 通过
subscribe(listener)
返回的函数注销监听器。
再次强调一下 Redux 应用只有一个单一的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。
根据已有的 reducer 来创建 store 是非常容易的。在前一个章节中,我们使用 combineReducers()
将多个 reducer 合并成为一个。现在我们将其导入,并传递 createStore()
。
1 | import { createStore } from 'redux' |
createStore()
的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。
1 | let store = createStore(todoApp, window.STATE_FROM_SERVER) |
几个问题
到现在,Redux的基础概念和用法讲完了,我们还是存在几个问题
- 到目前为止,好像React和Redux并没有任何联动,不存在什么关系,最起码用法上没有什么特殊照顾。
- Redux看起来只是个状态管理,并不是全局状态管理,那么react中为什么可以做到全局状态管理呢?
- 如何实现异步的Action,如果我就想在触发Action之后的Reducer中执行一些副作用怎么办?
我们一个个解决
搭配React
这里需要再强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。
Redux 默认并不包含 React 绑定库,需要单独安装。
1 | npm install --save react-redux |
如果你不使用npm,你也可以从unpkg获取最新的UMD包(包括开发环境包和生产环境包)。如果你用 <script>
标签的方式引入UMD包,那么它会在全局抛出window.ReactRedux
对象。
容器组件(Smart/Container Components)和展示组件(Dumb/Presentational Components)
Redux 的 React 绑定库是基于 容器组件和展示组件相分离 的开发思想。所以建议先读完这篇文章再回来继续学习。这个思想非常重要。
这篇文章是在hook出现之前出现的,也是class时代最为流行的开发方式,但是当hook出现后,作者承认hook是一种更好的实践,但是如果你的版本不支持hook,这种思想仍然十分重要。
那让我们再总结一下不同点:
展示组件 | 容器组件 | |
---|---|---|
作用 | 描述如何展现(骨架、样式) | 描述如何运行(数据获取、状态更新) |
直接使用 Redux | 否 | 是 |
数据来源 | props | 监听 Redux state |
数据修改 | 从 props 调用回调函数 | 向 Redux 派发 actions |
调用方式 | 手动 | 通常由 React Redux 生成 |
大部分的组件都应该是展示型的,但一般需要少数的几个容器组件把它们和 Redux store 连接起来。这和下面的设计简介并不意味着容器组件必须位于组件树的最顶层。如果一个容器组件变得太复杂(例如,它有大量的嵌套组件以及传递数不尽的回调函数),那么在组件树中引入另一个容器,就像FAQ中提到的那样
技术上讲你可以直接使用 store.subscribe()
来编写容器组件。但不建议这么做的原因是无法使用 React Redux 带来的性能优化。也因此,不要手写容器组件,而使用 React Redux 的 connect()
方法来生成,后面会详细介绍。
为什么所有组件都可以访问到redux的state
所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store
把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了一个容器组件。
建议的方式是使用指定的 React Redux 组件 `` 来 魔法般的 让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。
index.js
1 | import React from 'react' |
middleware创建异步action,有副作用的reducer
标准的做法是使用 Redux Thunk 中间件。要引入 redux-thunk
这个专门的库才能使用。我们 后面 会介绍 middleware 大体上是如何工作的;目前,你只需要知道一个要点:通过使用指定的 middleware,action 创建函数除了返回 action 对象外还可以返回函数。这时,这个 action 创建函数就成为了 thunk。
当 action 创建函数返回函数时,这个函数会被 Redux Thunk middleware 执行。这个函数并不需要保持纯净;它还可以带有副作用,包括执行异步 API 请求。这个函数还可以 dispatch action,就像 dispatch 前面定义的同步 action 一样。
我们仍可以在 actions.js
里定义这些特殊的 thunk action 创建函数。
actions.js
1 | import fetch from 'cross-fetch' |
我们是如何在 dispatch 机制中引入 Redux Thunk middleware 的呢?我们使用了 applyMiddleware()
,如下:
index.js
1 | import thunkMiddleware from 'redux-thunk' |
thunk 的一个优点是它的结果可以再次被 dispatch:
actions.js
1 | import fetch from 'cross-fetch' |
这可以让我们逐步开发复杂的异步控制流,同时保持代码整洁如初:
index.js
1 | store |