blog
blog copied to clipboard
Redux 入门摘要
原文: Redux 官方文档 EN | Redux 官方文档 CN 1. 有删减重组,需要细读的请直接浏览官方文档。 2. 专注 Redux 核心概念和开发流程,希望可以通过 15-20 分钟的阅读,对 Redux 有比较全面的了解,可以快速上手。
Redux 入门介绍
Redux is a predictable state container for JavaScript apps.
Redux 是一个给JavaScript app使用的可预测的状态容器。
为什么需要Redux?(动机)
JavaScript单页应用越来越复杂,代码必须管理远比以前多的状态(state)。这个状态包括服务端返回数据,缓存数据,本地创建的数据(未同步到服务器);也包括UI状态,如需要管理激活的路由,选中的标签,是否显示加载动效或者分页器等等。
管理不断变化的状态是很难的。如果一个 model 可以更新另一个 model ,那么一个 view 也可以更新一个 model 并导致另一个 model 更新,然后相应地,可能导致另一个 view 更新 —— 你理不清你的 app 发生了什么,失去了对 state 什么时候,为什么,怎么变化的控制 。当系统变得 不透明和不确定,就很难去重现 bug 和增加 feature 了。
通过 限制何时以及怎么更新,Redux 试图让 state 的变化可以预测 。
这里可以配合阅读 You Might Not Need Redux : Redux 的引入并不一定改善开发体验,必须权衡它的限制与好处。
Redux本身很简单,我们下面首先阐述它的核心概念和三大原则。
核心概念
想象一下用普通 JavaScript 对象 来描述 app 的 state:
// 一个 todo app 的 state 可能是这样的:
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}
这个对象就像没有 setter 的 model,所以其它部分的代码不能随意修改它而造成难以复现的 bug 。
如果要改变 state ,我们必须 dispatch 一个 action。action 是描述发生了什么的普通 JavaScript 对象。
// 下面都是action:
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }
强制 每个 change 都必须用 action 来描述,可以让我们清楚 app 里正在发生什么, state 是为什么改变的。最后,把 state 和 actions 联结起来,我们需要 reducer 。
reducer 就是函数,以之前的 state 和 action 为参数,返回新的 state :
// 关注 visibilityFilter 的 reducer
function visibilityFilter(state = 'SHOW_ALL', action) {
if (action.type === 'SET_VISIBILITY_FILTER') {
return action.filter;
} else {
return state;
}
}
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
};
}
以上就是 Redux 的核心概念,注意到我们并没有用任何 Redux 的 API,没加入任何 魔法。 Redux 里有一些工具来简化这种模式,但是主要的想法是描述如何根据这些 action 对象来更新 state。
三大原则
1. 单一数据源
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
console.log(store.getState())
/* Prints
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
*/
2. State 是只读的
改变 state 的唯一方式是触发 (emit) action,action 是描述发生了什么的对象。 这确保了视图和网络请求等都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心 race condition 的出现。
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})
3. 使用纯函数来执行修改
为描述 action 怎么改变 state tree,你要编写 reducers。
Reducer 只是一些纯函数,它接收之前的 state 和 action,并返回新的 state。刚开始你可能只需要一个 reducer ,但随着应用变大,你会需要拆分 reducer 。
以 todo app 为例迅速上手 Redux
1. 定义 actions
Action 就是把数据从应用(这些数据有可能是服务器响应,用户输入或其它非 view 的数据)发送到 store 的有效载荷。 它是 store 数据的唯一来源,你通过 store.dispatch(action) 来发送它到 store。
添加新 todo 任务的 action 是这样的:
const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
Action 本质上是 JavaScript 普通对象。Action 必须有一个字符串类型的 type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块/文件来存放 action。
除了 type 字段外,action 对象的结构完全由你自己决定。但通常,我们希望减少 action 中传递的数据。
Action 创建函数 (action creator)
Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。
// 生成一个 ADD_TODO 类型的 action
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
2. Reducers
Action 只是描述了有事情发生了这一事实,并没有指明应用如何更新 state。而这正是 reducer 要做的事情。
设计 State 结构
在 Redux 应用中,所有的 state 都被保存在一个单一对象中。最好可以在写代码之前想好 state tree 应该是什么形状的。
通常,这个 state tree 需要存放一些数据,以及一些 UI 相关的 state。这样做没问题,但尽量把数据与 UI 相关的 state 分开。
// todo app 的 state
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
处理 Action
有了 state 结构后,我们可以来写 reducer 了。 reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。
(previousState, action) => newState
保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:
- 修改传入参数;
- 执行有副作用的操作,如 API 请求和路由跳转;
- 调用非纯函数,如
Date.now()或Math.random()。
在高级篇里会介绍如何执行有副作用的操作。现在只需要记住 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
import { VisibilityFilters } from './actions'
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if(index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
})
default:
return state
}
}
注意:
- 不要修改 state。 使用
Object.assign({}, ...)新建了一个副本。 - 在
default情况下返回旧的 state。 遇到未知的 action 时,一定要返回旧的 state。
我们看到,多个 action 下,reducer 开始变得复杂。是否可以更通俗易懂?这里的 todos 和 visibilityFilter 的更新看起来是相互独立的,我们可以尝试拆分到单独的函数里。
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: todos(state.todos, action)
})
default:
return state
}
}
注意 todos 依旧接收 state,但它变成了一个数组!现在 todoApp 只把需要更新的一部分 state 传给 todos 函数,todos 函数自己确定如何更新这部分数据。这就是所谓的 reducer 合成,它是开发 Redux 应用最基础的模式。
现在更进一步,把 visibilityFilter 独立出去。那么我们可以有个主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 不再需要知道完整的 initial state。初始时,如果传入 undefined, 子 reducer 将负责返回它们(负责部分)的默认值。
// 彻底地拆分:
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。
当应用越来越复杂,我们还可以将拆分后的 reducer 放到不同的文件中, 以保持其独立性并用于专门处理不同的数据域。
最后,Redux 提供了 combineReducers() 工具来做上面 todoApp 做的事情。可以用它这样重构 todoApp:
import { combineReducers } from 'redux';
const todoApp = combineReducers({
visibilityFilter,
todos
})
// 完全等价于
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
3. 创建 store
前面两小节中,我们学会了使用 action 来描述“发生了什么”,和使用 reducers 来根据 action 更新 state 的用法。
Store 就是把它们联系到一起的对象。Store 有以下职责:
- 维持应用的 state;
- 提供
getState()方法获取 state; - 提供
dispatch(action)方法更新 state; - 通过
subscribe(listener)注册监听器; - 通过
subscribe(listener)返回的函数注销监听器。
再次强调一下 Redux 应用只有一个 单一 的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。
根据已有的 reducer 来创建 store 是非常容易的。在前面我们使用 combineReducers() 将多个 reducer 合并成为一个。现在我们将其导入,并传给 createStore()。
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
你可以把初始状态 intialState 作为第二个参数传给 createStore()。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。
let store = createStore(todoApp, window.STATE_FROM_SERVER)
发起 actions
现在我们已经创建好了 store ,可以验证一下:
import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions'
// 打印初始状态
console.log(store.getState())
// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
// 发起一系列 action
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
// 停止监听 state 更新
unsubscribe();
4. 数据流
严格的单向数据流 是 Redux 架构的设计核心。
这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个且独立的无法相互引用的重复数据。
Redux 应用中数据的生命周期遵循下面 4 个步骤:
-
调用
store.dispatch(action) -
Redux store 调用传入的 reducer 函数。
-
根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
-
Redux store 保存了根 reducer 返回的完整 state 树。
这个新的树就是应用的下一个 state!所有订阅 store.subscribe(listener) 的监听器都将被调用;监听器里可以调用 store.getState() 获得当前 state。
搭配 React 一起使用
首先强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。
尽管如此,Redux 还是和 React 和 Deku 这类框架搭配起来用最好,因为这类框架允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。
安装 react-redux
Redux 自身并不包含对 React 的绑定库,我们需要单独安装 react-redux。
Presentational and Container Components
绑定库是基于 容器组件和展示组件相分离 的开发思想。建议先读完这篇文章。
| 展示组件 | 容器组件 | |
|---|---|---|
| 作用 | 描述如何展现(骨架、样式) | 描述如何运行(数据获取、状态更新) |
| 直接使用 Redux | 否 | 是 |
| 数据来源 | props | 监听 Redux state |
| 数据修改 | 从 props 调用回调函数 | 向 Redux 派发 actions |
| 调用方式 | 手动 | 通常由 React Redux 生成 |
技术上讲,我们可以手动用 store.subscribe() 来编写容器组件,但这就无法使用 React Redux 做的大量性能优化了。一般使用 React Redux 的 connect() 方法来生成容器组件。(不必为了性能而手动实现 shouldComponentUpdate 方法)
设计组件层次结构
还记得前面 设计 state 根对象的结构 吗?现在就要定义与它匹配的界面的层次结构。这不是 Redux 相关的工作,React 开发思想在这方面解释的非常棒。
-
展示组件: 纯粹的UI组件,定义外观而不关心数据怎么来,怎么变。传入什么就渲染什么。
-
容器组件: 把展示组件连接到 Redux。监听 Redux store 变化并处理如何过滤出要显示的数据。
-
其它组件 有时很难分清到底该使用容器组件还是展示组件,并且组件并不复杂,这时可以混合使用。
实现组件
省略其它部分,主要讲讲容器组件一般怎么写。
import { connect } from 'react-redux'
// 3. connect 生成 容器组件
const ContainerComponent = connect(
mapStateToProps,
mapDispatchToProps
)(PresentationalComponent)
// 2. mapStateToProps 指定如何把当前 Redux store state 映射到展示组件的 props 中
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
// 1. mapDispatchToProps() 方法接收 dispatch() 方法并返回期望注入到展示组件的 props 中的回调方法。
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
// 可以使用 Redux 的 bindActionCreators 把所有的暴露出来的 actionCreators 转成方法注入 props
export default ContainerComponent
connect 本身还是很明确的,指定我们注入哪些 data 和 function 到展示组件的 props ,给展示组件使用。
API 探索
Redux API
1. createStore(reducer, [preloadedState], enhancer)
创建一个 Redux store 来以存放应用中所有的 state。详情可见 Redux API,这里主要强调两点:
preloadedState:初始时的 state。在同构中会用到,比如从一个session恢复数据。
当 store 创建后,Redux 会 dispatch action({ type: ActionTypes.INIT })) 到 reducer 上,得到初始的 state 来填充 store。所以你的初始 state 是 preloadedState 在 reducers 处理 ActionTypes.INIT action 后的结果。 https://github.com/reactjs/redux/blob/v3.6.0/src/createStore.js#L241-L244
enhancer:如果有enhancer,那么会首先得到增强的createStore,然后再createStore(reducer, [preloadedState])。
https://github.com/reactjs/redux/blob/v3.6.0/src/createStore.js#L50 可结合下面 applyMiddleware一起看。
2. middleware 与 applyMiddleware(...middlewares)
我们可以用 middleware 来扩展 Redux。Middleware 可以让你包装 store 的 dispatch 方法来达到你想要的目的。同时, middleware 还拥有“可组合”这一关键特性。多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。
middleware 的函数签名是 ({ getState, dispatch }) => next => action。
如下是两个 middleware:
// logger middleware
function logger({ getState }) {
return (next) => (action) => {
console.log('will dispatch', action)
// 调用 middleware 链中下一个 middleware 的 dispatch。
let returnValue = next(action)
console.log('state after dispatch', getState())
// 一般会是 action 本身,除非
// 后面的 middleware 修改了它。
return returnValue
}
}
// thunk middleware
function thunk({ dispatch, getState }) {
return (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}
}
applyMiddleware 返回一个应用了 middleware 后的 store enhancer。这个 store enhancer 的签名是 createStore => createStore,但是最简单的使用方法就是直接作为最后一个 enhancer 参数传递给 createStore() 函数。
再来看下 applyMiddleware:
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer)
var dispatch = store.dispatch
var chain = []
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
// chain 是 [(next) => (action) => action, ...]
chain = middlewares.map(middleware => middleware(middlewareAPI))
// compose(...chain) 返回这样一个函数:
// 对 chain 进行 reduce,从右向左执行,每次的结果作为下次执行的输入
dispatch = compose(...chain)(store.dispatch)
// 最终的 dispatch 是这样的:(action) => action
return {
...store,
dispatch
}
}
}
可以看到(假设 enhancerTest = applyMiddleware(A, B, C)):
- middleware 其实只是劫持/包装了
dispatch。 dispatch本质上是同步的,但我们可以通过 thunk 等延迟执行dispatch。chain[index](dispatch) --> (action) => action,即我们得到的dispatch是一个层层嵌套的(action) => action函数。- 除了最右侧的 C 得到的
next是原本的dispatch,剩下的都是被层层嵌套的(action) => action函数,并且越右侧越嵌套在里面,所以当dispatch(action)调用时,将会以下面顺序执行:A -> B -> C -> B -> A。C之前A/B都只执行了next之前的逻辑,之后各自完全执行。
3. combineReducers(reducers)
把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数。
真的很简单,从逻辑上来讲,就是:
combineReducers({
keyA: reducerA,
keyB: reducerB
})
// --->
function recuderAll (prevState, action) {
return {
keyA: reducerA(prevState.keyA, action),
keyB: reducerB(prevState.keyB, action)
}
}
核心就是干了上面的事,只是多了一些判断和检查。
4. bindActionCreators(actionCreators, dispatch)
把 action creators 转成拥有同名 keys 的对象,但使用 dispatch 把每个 action creator 包围起来,这样可以直接调用它们。
const actionCreators = {
updateOrAddFilter(filter) {
type: UPDATE_OR_ADD_FILTER,
filter
},
removeFilter(type) {
type: REMOVE_FILTER,
filterType: type
}
}
bindActionCreators(actionCreators, dispatch)
// -->
{
updateOrAddFilter: (...args) => dispatch(original_updateOrAddFilter(...args)),
removeFilter: (...args) => dispatch(original_removeFilter(...args)),
}
核心就是自动 dispatch ,这样我们可以在 react 组件里直接调用,Redux store 就能收到 action。
React Redux API
1. Provider
用法:
ReactDOM.render(
<Provider store={store}>
<MyRootComponent />
</Provider>,
rootEl
)
源码:
// storeKey 默认是 'store'
class Provider extends Component {
getChildContext() {
return { [storeKey]: this[storeKey], [subscriptionKey]: null }
}
constructor(props, context) {
super(props, context)
this[storeKey] = props.store;
}
render() {
return Children.only(this.props.children)
}
}
Provider.propTypes = {
store: storeShape.isRequired,
children: PropTypes.element.isRequired,
}
Provider.childContextTypes = {
[storeKey]: storeShape.isRequired,
[subscriptionKey]: subscriptionShape,
}
注意到, Provider 应用了 React Context,子组件都可以去访问 store 。
2. connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
connect 的函数签名是 ([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) => (WrappedComponent) => ConnectComponent,最后返回的 onnectComponent 可以通过 context 去访问 store。
connect API 比较复杂,这里主要讲下前两个参数。
[mapStateToProps(state, [ownProps]): stateProps] (Function): 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。如果你省略了这个参数,你的组件将不会监听 Redux store。[mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function): 如果传递的是一个对象,那么每个定义在该对象的函数都将被当作 Redux action creator,而且这个对象会与 Redux store 绑定在一起,其中所定义的方法名将作为属性名,合并到组件的 props 中。