Nancy's Studio.

Redux学习笔记

Word count: 4,191 / Reading time: 18 min
2019/07/25 Share

介绍

前述

Redux:JavaScript 状态容器,可预测的状态管理机 。Redux的思想继承自Facebook的Flux架构,但比Flux更加简洁易用。

安装稳定版:

1
npm install --save redux

多数情况下,还需要使用React 绑定库和开发者工具

1
2
npm install --save react-redux
npm install --save-dev redux-devtools

主要组成

Redux应用的主要组成为:action、reducer、store。

  1. action:是Redux中信息的载体, store 的唯一信息来源。一般通过 store.dispatch() 将 action 传到 store。

  2. reducer:根据action做出相应响应,决定如何修改应用的状态state。需要在编写reducer前设计好state,state包含服务器获取的数据和UI状态。

    reducer是一个纯函数,它接受两个参数,当前的state和action,返回新的state。

    (previousState, action) => newState

  3. store:是Redux的一个对象,也是action和reducer之间的桥梁。主要负责:

    • 保存应用状态
    • 通过方法getState()访问应用状态
    • 通过方法dispatch(action)发送更新状态的意图
    • 通过方法subscribe(listener)注册监听函数、监听应用状态的改变

    一个Redux应用只有一个store,store保存了唯一数据源。store通过createStore()创建,创建时需要传递reducer作为参数。

主要组成部分之间关系的简单示例

当使用普通对象来描述应用的 state 时。例如,todo 应用的 state 可能长这样:

1
2
3
4
5
6
7
8
9
10
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}

要想更新 state 中的数据,需要发起一个 action。Action 就是一个普通 JavaScript 对象,用来描述发生了什么。下面是一些 action 的示例:

1
2
3
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

强制使用 action 来描述所有变化带来的好处是可以清晰地知道应用中到底发生了什么。如果一些东西改变了,就可以知道为什么变。action 就像是描述发生了什么的指示器。最终,为了把 action 和 state 串起来,我们需要开发一些函数,也就是 reducer。reducer 是接收 state 和 action作为参数并返回新的 state 的函数。 对于较大的应用来说,不可能仅仅写一个这样的函数,所以我们需要编写很多小函数来分别管理 state 的一部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }])
case 'TOGGLE_TODO':
return state.map((todo, index) =>
action.index === index
? { text: todo.text, completed: !todo.completed }
: todo
)
default:
return state
}
}

function visibilityFilter(state = 'SHOW_ALL', action) {
if (action.type === 'SET_VISIBILITY_FILTER') {
return action.filter
} else {
return state
}
}

可以再构建一个 reducer 调用上面两个 reducer 以管理整个应用的 state:

1
2
3
4
5
6
function todoApp(state = {}, action) {
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
}
}

三大原则

单一数据源

整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

这让同构应用开发变得非常容易。来自服务端的 state 可以在无需编写更多代码的情况下被序列化并注入到客户端中。由于是单一的 state tree ,调试也变得非常容易。在开发中,可以把应用的 state 保存在本地以加快开发速度。此外,受益于单一的 state tree ,以前难以实现的如“撤销/重做”这类功能也变得轻而易举。

State 只读

唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

这样确保了视图和网络请求都不能直接修改 state,它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照顺序执行,因此不用担心竞态条件(race condition)的出现。 Action 就是普通对象而已,它们可以被日志打印、序列化、储存、或在测试时回放出来。

使用纯函数执行修改

为了描述 action 如何改变 state tree ,我们需要编写 reducers。

Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。刚开始可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分。因为 reducer 只是函数,我们可以控制它们被调用的顺序,传入附加数据,编写可复用的 reducer 来处理一些通用任务,如分页器。

Action

action用于描述应用发生了什么操作。

构建

Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。

除了 type 字段外,action 对象的结构由实际业务场景决定(可参照 Flux 标准 Action 获取关于如何构造 action 的建议)。

我们还需要再添加一个 action index 来表示用户完成任务的动作序列号。因为数据是存放在数组中的,所以我们通过下标 index 来引用特定的任务。而实际项目中一般会在新建数据的时候生成唯一的 ID 作为数据的引用标识。

通过action creator创建action,如下所示是以todos应用为示例的actions.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*actions.js*/
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

//筛选todo列表的filters
export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
}

//action creators
export function addTodo (text) {
return { type: ADD_TODO, text }
}
export function toggleTodo (index) {
return { type: TOGGLE_TODO, index }
}
export function setVisibilityFilter (filter) {
return { type: SET_VISIBILITY_FILTER, filter }
}

简化

可以看到action creators的这些function都很相似,我们可以构造一个用于生成 action creator 的函数来简化样板代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function makeActionCreator(type, ...argNames) {
return function(...args) {
const action = { type }
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index]
})
return action
}
}

//定义常量
const ADD_TODO = 'ADD_TODO'
const EDIT_TODO = 'EDIT_TODO'
const REMOVE_TODO = 'REMOVE_TODO'

//使用makeActionCreator
export const addTodo = makeActionCreator(ADD_TODO, 'text')
export const editTodo = makeActionCreator(EDIT_TODO, 'id', 'text')
export const removeTodo = makeActionCreator(REMOVE_TODO, 'id')

Reducer

reducer根据action做出响应,决定如何修改应用的状态state。

构建

首先设计state,以todos应用为例:

1
2
3
4
5
6
7
8
9
10
{
todos: [{
text: 'Learn React',
completed: true
}, {
text: 'Learn Redux',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}

然后尝试创建一个最基本的reducer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { VisibilityFilters } from './actions'

const initialState = {
todos: [],
visibilityFilter: VisibilityFilters.SHOW_ALL
}

//reducer
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return { ...state, visibilityFilter: action.filter }
case ADD_TODO:
return { ...state,
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
}
case TOGGLE_TODO:
return { ...state,
todos: state.todos.map(
(todo, index) => {
if(index === action.index) {
return { ...todo, completed: !todo.completed }
}
})
}
default:
return state
}
}

注:除了利用扩展运算符(…)创建新的state对象,还可以使用ES6的Object.assign()

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象并返回目标对象。 语法:Object.assign(target, …sources);

1
2
3
4
5
6
7
8
9
10
11
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
//...其他的case也类似
default:
return state
}
}

上面我们使用todoApp一个reducer处理所有的action,当应用变得复杂时,todoApp也会变得更加复杂,这时需要拆分出多个reducer,每个reducer处理state中的部分状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//处理todo的reducer
function todos (state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }])
case 'TOGGLE_TODO':
return state.map(
(todo, index) => {
action.index === index
? { ...todo, completed: !todo.completed }
: todo
}
)
default:
return state
}
}

//处理visibilityFilter的reducer
function visibilityFilter (state = 'SHOW_ALL', action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}

//简化后的todoApp
function todoApp (state = {}, action) {
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
}
}

Redux提供了一个combineReducers()函数,用于合并多个reducer。可以使用combineReducers()对todoApp进行进一步改写:

1
2
3
4
5
6
import { combineReducers } from 'redux'

const todoApp = combineReducers({
todos,
visibilityFilter
})

简化

与action creater部分的简化方式类似,我们可以创建一个Reducers生成器,将action types映射到对应的handlers,取代之前用的switch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createReducer (initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}

//使用createReducer
export const todos = createReducer([], {
[ActionTypes.ADD_TODO]: (state, action) => { //这里传入的state是state.todos
const text = action.text.trim()
return [...state, { text: text, completed: false }]
}
})

注意点

!永远不要在 reducer 里做这些操作:

  • 修改传入参数;
  • 执行有副作用的操作,如 API 请求和路由跳转;
  • 调用非纯函数,如 Date.now()Math.random()

    reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况,没有副作用,没有 API 请求,没有变量修改,单纯执行计算。

Store

Store 是把action和reducer联系到一起的桥梁。Store 有以下职责:

  • 维持应用的 state;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action)方法更新 state;
  • 通过 subscribe(listener)注册监听器;
  • 通过 subscribe(listener)返回的函数注销监听器。

再次注意 Redux 应用只有一个store。当需要拆分数据处理逻辑时,应该使用 reducer 组合而不是创建多个 store。

构建

1
2
3
4
5
6
import { createStore } from 'redux'
import todoApp from './reducers'

let store = createStore(todoApp)
//注:createStore的第二个参数是可选的,用于设置state的初始状态
let store = createStore(todoApp, initialState)

发起Actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {
addTodo,
toggleTodo,
setVisibilityFilter,
VisibilityFilters
} from './actions'

//打印初始状态
console.log(store.getState())

//每次 state 更新时,打印日志
//subscribe() 返回一个函数用来注销监听器
const unsubscribe = store.subscribe(() => console.log(store.getState()))

//发起actions
store.dispatch(addTodo('learn actions'))
store.dispatch(addTodo('learn reducers'))
store.dispatch(addTodo('learn store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

//停止监听 state 更新
unsubscribe()

数据流

严格的单向数据流是 Redux 架构的设计核心。 这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个且独立的无法相互引用的重复数据。

Redux 应用中数据的生命周期遵循下面 4 个步骤:

  1. 调用 store.dispatch(action)
  2. store 调用传入的 reducer 函数。
  3. 根 reducer 把多个子 reducer 的输出合并成单一的 state 树。
  4. store 保存根 reducer 返回的完整 state 树。

新的树就是应用的下一个 state,所有订阅 store.subscribe(listener)的监听器都将被调用;监听器里可以调用 store.getState() 获得当前 state,可以应用新的 state 来更新 UI。

在React中使用Redux

展示组件与容器组件

展示组件 容器组件
作用 描述如何展现(骨架、样式) 描述如何运行(数据获取、状态更新)
直接使用 Redux
数据来源 props 监听 Redux state
数据修改 从 props 调用回调函数 向 Redux 派发 actions
调用方式 手动 通常由 React Redux 生成

展示组件

展示组件只定义外观而不考虑数据来源和如何改变。如果把代码从 Redux 迁移到别的架构,这些组件可以不做任何改动直接使用,它们并不依赖于 Redux。

容器组件

容器组件用于把展示组件连接到 Redux 。例如,展示型的 TodoList 组件需要一个类似 VisibleTodoList 的容器来监听 Redux store 变化并处理如何过滤出要显示的数据,VisibleTodoList根据当前显示的状态来对 todo 列表进行过滤,并渲染 TodoList

connect

react-redux提供了一个connect函数,用于把React组件和Redux store连接起来,生成一个容器组件,负责数据管理和业务逻辑。

1
2
3
4
import { connect } from 'react-redux'
import TodoList from './TodoList'

const VisibleTodoList = connect()(TodoList);

在这里VisibleTodoList需要承担两个工作:

  1. 从 Redux store 中获取展示组件所需的应用状态
  2. 把展示组件的状态变化同步到 store 中

通过为connect传递两个函数作为参数可以让VisibleTodoList具备这两个功能:

1
2
3
4
5
6
7
import { connect } from 'react-redux'
import TodoList from './TodoList'

const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList);

mapStateToProps

第一个函数的作用是把state转换成props,state是store中保存的应用状态,它会作为参数传递给mapStateToProps,props就是被连接的展示组件的props。

以筛选Todos列表为例:

1
//

每当store中的state更新时mapStateToProps就会重新执行传给组件新的props。

mapStateToProps除了接收state参数外还可以传入第二个参数作为容器组件的props对象。

mapDispatchToProps

第二个函数的作用是发送action更新state,函数接收store.dispatch作为参数并返回展示组件用来修改state的函数。

1
2
3
4
5
6
7
8
9
10
11
function toggleTodo(id) {
return { type: 'TOGGLE_TODO', id }
}

function mapDispatchToProps(dispatch) {
return {
onTodoClick: function(id) {
dispatch(toggleTodo(id))
}
}
}

这样展示组件就可以调用this.props.onTodoClick(id)发送修改待办事项状态的action了。与mapStateToProps相同,mapDispatchToProps也支持第二个参数代表容器组件的props。

Provider组件

通过connect创建出的容器组件通过Provider组件获取Redux store。Provider需要接收一个store属性,并把store属性保存到context,Provider通过context把store传递给子组件。

所以一般把Provider组件作为根组件,使内层的任意组件可以从context中获取store对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { render } from 'react-dom'
import { createStore } from 'react-redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp)

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);

中间件

Redux middleware 提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。 可以利用 Redux middleware 来进行日志记录、创建错误报告、调用异步接口或者路由等等。

现在模拟日志记录和打印错误信息的中间件:(抽象并整合后的形式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}

const errReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}

引用方式:

1
2
3
4
5
6
7
8
import { createStore, combineReducers, applyMiddleware } from 'redux'

const todoApp = combineReducers(reducers)
const store = createStore(
todoApp,
// applyMiddleware() 告诉 createStore() 如何处理中间件
applyMiddleware(logger, errReporter)
)

现在任何被发送到 store 的 action 都会经过 loggererrReporter

1
2
// 将经过 logger 和 errReporter 两个中间件
store.dispatch(addTodo('Use Middlewares'))

applyMiddleware()的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default function applyMiddleware (...middlewares) {
return (createStore) => (...args) => {
const store = createStore(...args)
let dispatch = store.dispatch
let chain = []

const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

return {
...store,
dispatch
}

}
}

compose(f,g,h)等价于(…args) => f(g(h(args)))

异步

Redux中处理异步操作需要借助中间件的帮助,redux-thunk是处理异步操作最常用的中间件。可以使用 applyMiddleware() 来增强 createStore()

1
2
3
4
5
6
7
8
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducers'

const store = createStore(
reducer,
applyMiddleware(thunk)
);

处理一个网络请求会使用三个action,分别表示请求开始、请求成功和请求失败。

1
2
3
{ type: 'FETCH_DATA_REQUEST' }
{ type: 'FETCH_DATA_SUCCESS', data: {...} }
{ type: 'FETCH_DATA_FAILURE', error: 'something wrong...' }

定义一个异步action模拟向服务器请求数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function getData (url) {
return function (dispatch) {
dispatch({ type: 'FETCH_DATA_REQUEST' })
return fetch (url)
.then(
res => res.json(),
err => {
console.log(err)
dispatch({ type: 'FETCH_DATA_FAILURE', error: 'something wrong...' })
}
)
.then(json =>
dispatch({type: 'FETCH_DATA_SUCCESS', data: json})
)
}
}

store.dispatch(getData(url));

异步数据流的处理:

redux-thunkredux-promise 这样支持异步的 middleware 都包装了 store 的 dispatch()方法,可以 dispatch 一些除了 action 以外的其他内容,例如:函数或者 Promise。使用的任何 middleware 都可以通过自己的方式解析 dispatch 的内容并继续传递 actions 给下一个 middleware。

middleware 链中的最后一个中间件 dispatch 的 action 必须是一个普通对象。我们可以使用任意多异步的 middleware 去做想做的事情,但是需要使用普通对象作为最后一个被 dispatch 的 action ,来将处理流程带回同步方式。

参考及学习资料

  1. 《React进阶之路》—— 徐超
  2. 《深入React技术栈》—— 陈屹
  3. https://github.com/brickspert/blog/issues/22 通俗易懂的好文
  4. http://cn.redux.js.org Redux中文文档
CATALOG
  1. 1. 介绍
    1. 1.1. 前述
    2. 1.2. 主要组成
    3. 1.3. 主要组成部分之间关系的简单示例
    4. 1.4. 三大原则
      1. 1.4.1. 单一数据源
      2. 1.4.2. State 只读
      3. 1.4.3. 使用纯函数执行修改
  2. 2. Action
    1. 2.1. 构建
    2. 2.2. 简化
  3. 3. Reducer
    1. 3.1. 构建
    2. 3.2. 简化
    3. 3.3. 注意点
  4. 4. Store
    1. 4.1. 构建
    2. 4.2. 发起Actions
  5. 5. 数据流
  6. 6. 在React中使用Redux
    1. 6.1. 展示组件与容器组件
      1. 6.1.1. 展示组件
      2. 6.1.2. 容器组件
    2. 6.2. connect
      1. 6.2.1. mapStateToProps
      2. 6.2.2. mapDispatchToProps
    3. 6.3. Provider组件
  7. 7. 中间件
  8. 8. 异步
  9. 9. 参考及学习资料