https://www.redux.org.cn/

知识铺垫

SPA(Single Page Application)

那么SPA和之前的网页有什么区别呢?其实最本质的区别就是服务端渲染前端渲染的区别。

  • 服务端渲染,是指浏览器请求一个URL地址,服务端返回的HTML页面是完整的,浏览器直接展示这个HTML内容再加上CSS的样式即可。JS主要做一些辅助性的特效或其他工作。
  • 前端渲染,是指浏览器请求一个URL地址,服务端返回的HTML页面并不包含具体内容,而是会通过 script 标签引入一个JavaScript文件。浏览器只有通过解析和执行JS代码页面上才会展示出内容,否则页面就是空白的。而且后续的页面更新也都是在JS中完成,而不是通过跳转到另一个URL地址来完成(这也是单页应用名字的由来)。当然这里并不是指URL地址就一定不会变化,而是说页面不会“刷新”。

SPA的出现使得Web前端成为真正的”客户端程序”,可以独立完成渲染(DOM API)、网络请求(XHR,fetch)等任务,也就是所谓的”前端渲染”,而不是只是一个“网页展示器”。

声明式编程和命令式编程

  • 前端状态管理之所以流行的另一个原因是,现代的前端框架包括React/Vue都是声明式的编程方式,而之前的jQuery是命令式的。
  • 所谓声明式,就是说你在代码中不会直接去操作UI,而是通过操作数据,来间接地改变UI内容。而命令式,是直接操作UI的,这就导致数据层和展现层通常是不作区分的。

声明式的编码方式天然的会把数据状态和页面代码分离,所以更需要一套独立的状态管理系统。

什么是状态管理

  • 为什么需要状态管理:

    1. 因为有了单页应用的需求(为了解决浏览器刷新的用户体验问题),所以需要前端渲染;
    2. 因为有了前端渲染,所以前端不得不需要自己管理状态。
  • 前端状态的概念主要是应用在单页应用SPA中的,是在React/Vue等现代化的前端框架流行起来之后才有的一个提法,之前的jQuery时代是没有这种概念的。

  • 前端渲染的方式:

    1. 你在浏览器中输入了网址:https://todo.com/,浏览器向你的服务端发起HTTP请求,然后你的服务器返回HTML文件。这一步是和`服务器渲染`没有区别的,区别就在于服务端返回的那个HTML文件的内容。在服务端渲染的情况下,这个文件是由服务端“拼装”出来的,而在前端渲染的情况下,这个文件不包含任何内容,而只是引入了一个JS文件。比如下面这样的:

      1
      2
      3
      4
      5
      6
      7
      <html>
      <head>
      <script src="/app.js"></script>
      <head>
      <body>
      </body>
      </html>
    2. 当这个文件到达浏览器,因为HTML的body部分是空的,所以在执行JS之前页面会是空白的。接下来浏览器会下载和执行JS代码。

      在JS代码中,你会使用XHR的方式向服务器发起一个“API请求“,这类请求是浏览器在后台发起,不会引起地址栏和页面刷新。和页面请求不同,服务器对这种“API请求”的响应内容通常会是JSON格式的。比如,请求列表的URL是:GET /api/todos,它的响应是:

      1
      2
      3
      4
      5
      6
      7
      8
      [
      {
      "id": 1,
      "title": "title1",
      "content": "This is list 1 content",
      },
      ...
      ]
    3. 收到响应之后,JS代码会被执行,之后会由JS执行DOM操作来改变页面的展示内容,也就是页面内容被“渲染”出来了。

      列表页面中的每一条会是一个简单的<div>标签,而不是一个会发生浏览器跳转的<a>标签。当你点击其中一个标题的时候,同样会由JS代码接管。由于在请求列表的时候前端已经拥有了所有数据,所以这次不用再请求服务器了,JS代码直接重新操作DOM,来”渲染”出编辑页面即可。

      当在编辑页面中点击Save按钮,同样触发JS代码,JS代码会向服务器发起一个保存修改的请求。如果服务器响应成功,那么JS会更新自身的数据,修改为新的标题和内容。

    4. 在这个例子中,所谓的前端“状态”,可能就是一个全局的JS对象,比如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      {
      "todos": [
      {
      "id": 1,
      "title": "title1",
      "content": "This is list 1 content",
      },
      ...
      ]
      }

      而所谓的”状态管理”,其实就是对这个全局对象的一系列增删改查的操作。

  • 通过上面的例子,我们可以看一下前端状态管理的意义:

    • 数据管理逻辑和页面渲染逻辑分离,使得代码更容易维护。这其实有点类似于MVC的设计模式,操作数据的地方不会关心页面如何展示,展示页面的地方不会关心数据从哪里来的。

    • 可以保证数据有一份“唯一可信数据源“。

      比如上面例子中的“title”字段,在列表页面和编辑页面都会展示,如果不对这个状态做统一管理,很难保证数据的统一:比如在修改页面修改了之后要保证列表页面同步修改。在实际的项目中,同一份数据可能在N多个地方展示、修改,如果不做好状态管理,代码会写成什么样简直不敢想。

容器组件(Smart/Container Components)和展示组件(Dumb/Presentational Components)

如果将组件划分为两类,你会发现组件重用起来更加容易。我把这两类称为ContainerPresentational

首先我们来看一个容器组件和展示组件一起的例子吧。

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
class TodoList extends React.Component{
constructor(props){
super(props);
this.state ={
todos:[]
}
this.fetchData = this.fetchData.bind(this);
}
componentDidMount(){
this.fetchData();
}
fetchData(){
fetch('/api/todos').then(data =>{
this.setState({
todos:data
})
})
}
render(){
const {todos} = this.state;
return (<div>
<ul>
{todos.map((item,index)=>{
return <li key={item.id}>{item.name}</li>
})}
</ul>
</div>)
}
}

大家可以看到这个例子是没有办法复用的,因为数据的请求和数据的展示都在一个组件进行,要实现组件的复用,我们就需要将展示组件和容器组件分离出来。

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
36
37
38
39
40
41
//展示组件
class TodoList extends React.Component{
constructor(props){
super(props);
}
render(){
const {todos} = this.props;
return (<div>
<ul>
{todos.map((item,index)=>{
return <li key={item.id}>{item.name}</li>
})}
</ul>
</div>)
}

//容器组件
class TodoListContainer extends React.Component{
constructor(props){
super(props);
this.state = {
todos:[]
}
this.fetchData = this.fetchData.bind(this);
}
componentDidMount(){
this.fetchData();
}
fetchData(){
fetch('/api/todos').then(data =>{
this.setState({
todos:data
})
})
}
render(){
return (<div>
<TodoList todos={this.state.todos} />
</div>)
}
}

当我们把组件分离成容器组件和展示组件这两类时,你会发现他们能够很方便的实现复用。

presentational组件:

  • 关心事物如何展示;
  • 也许会同时包含展示类组件和容器组件,并且通常有一些DOM操作和自己的样式;
  • 通常允许this.props.chidren放在容器里;
  • 对应用的其余部分没有依赖,比如Flux actions或者stores
  • 不会说明数据是如何加载和变化的;
  • 只通过props接受数据和回调函数;
  • 几乎没有自己的state(即使有,也是UI相关的,而不是data相关的);
  • 通常被写作函数式组件除非需要状态、生命周期的钩子、性能优化;
  • 例子:Page, Sidebar, Story, UserInfo, List

container组件:

  • 关心事物如何运作;
  • 也许会同时包含展示类组件和容器组件,但是除了一些用来包含元素的div通常不会其他有DOM操作,并且不会包含任何样式;
  • presentational组件或其他container组件提供数据和行为;
  • 通常是有状态的,因为它们往往作为数据源;
  • 通常使用像React Reduxconnect()RelaycreateContainer()Flux UtilsContainer.create()这样的高阶组件来生成,而不是手动编写;
  • 例子:UserPage, FollowersSidebar, StoryContainer, FollowedUserList
展示组件 容器组件
作用 描述如何展现(骨架、样式) 描述如何运行(数据获取、状态更新)
直接使用 Redux
数据来源 props 监听 Redux state
数据修改 从 props 调用回调函数 向 Redux 派发 actions
调用方式 手动 通常由 React Redux 生成

大部分的组件都应该是展示型的,但一般需要少数的几个容器组件把它们和 Redux store 连接起来

优点(benifit)

  1. 展示和容器组件更好的分离,有助于更好的理解应用和UI
  2. 重用性高,展示组件可以用于多个不同数据源。
  3. 展示组件就是你的调色板,可以把他们放到单独的页面,在不影响应用程序的情况下,让设计师调整UI。
  4. 这迫使您提取诸如侧边栏,页面,上下文菜单等“布局组件”并使用this.props.children,而不是在多个容器组件中复制相同的标记和布局。

介绍

  • Redux 是 JavaScript 状态容器,提供可预测化的状态管理。
  • 可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。

在下面的场景中,引入 Redux 是比较明智的:

  • 你有着相当大量的、随时间变化的数据
  • 你的 state 需要有一个单一可靠数据来源
  • 你觉得把所有 state 放在最顶层组件中已经无法满足需要了

动机

  1. 随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理更多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。
  2. 管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。

为什么需要Redux?

  1. 在React中,数据在组件中是单向流动的。数据从一个方向父组件流向子组件(通过props),由于这个特征,两个非父子关系的组件(或者称作兄弟组件)之间的通信并不是那么清楚。
  2. React并不建议直接采用组件到组件的通信方式,尽管它有一些特性可以支持这么做(比如先将子组件的值传递给父组件,然后再由父组件在分发给指定的子组件)。这被很多人认为是糟糕的实践方式,因为这样的方式容易出错而且会让代码向“拉面”一样不容易理解。
  3. Redux的出现就让这个问题的解决变得更加方便了。Redux提供一种存储整个应用状态到一个地方的解决方案(可以理解为统一状态层),称为store,组件将状态的变化转发通知(dispatch)给store,而不是直接通知其它的组件。组件内部依赖的state的变化情况可以通过订阅store来实现。
  4. 使用Redux,所有的组件都从store里面获取它们依赖的state,同时也需要将state的变化告知store。组件不需要关注在这个store里面其它组件的state的变化情况,Redux让数据流变得更加简单。这种思想最初来自Flux,它是一种和React相同的单向数据流的设计模式。

img

核心概念

Redux的核心由三部分组成:Store, Action, Reducer

  • Store : 是个对象,贯穿你整个应用的数据都应该存储在这里。
  • Action: 是个对象,必须包含type这个属性,reducer将根据这个属性值来对store进行相应的处理。
  • Reducer:是个函数。接受两个参数:要修改的数据(state) 和 action对象。根据action.type来决定采用的操作,对state进行修改,最后返回新的state

调用关系如下所示:

1
store.dispatch(action) --> reducer(state, action) --> final state

store

store:

  • store在这里代表的是数据模型,内部维护了一个state变量,用例描述应用的状态。

  • store有两个核心方法,分别是getStatedispatch。前者用来获取store的状态(state),后者用来修改store的状态。

    • store.getState():获取store的状态
    • store.dispatch(action):根据action来修改store,返回最新的state
  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 创建store, 传入两个参数
    // 参数1: reducer 用来修改state
    // 参数2(可选): [], 默认的state值,如果不传, 则为undefined
    var store = redux.createStore(reducer, []);

    // 通过 store.getState() 可以获取当前store的状态(state)
    // 默认的值是 createStore 传入的第二个参数
    console.log('state is: ' + store.getState()); // state is:

    // 通过 store.dispatch(action) 来达到修改 state 的目的
    // 注意: 在redux里,唯一能够修改state的方法,就是通过 store.dispatch(action)
    store.dispatch({type: 'add_todo', text: '读书'});

当使用普通对象来描述应用的 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'
}

这个对象就像 “Model”,区别是它并没有 setter(修改器方法)。因此其它的代码不能随意修改它,造成难以复现的 bug。

action

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

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

reduers

  • action 就像是描述发生了什么的指示器。最终,为了把 action 和 state 串起来,开发一些函数,这就是 reducer。
  • reducer 只是一个接收 state 和 action,并返回新的 state 的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function visibilityFilter(state = 'SHOW_ALL', action) {
if (action.type === 'SET_VISIBILITY_FILTER') {
return action.filter
} else {
return state
}
}

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
}
}

再开发一个reducer 调用这两个 reducer,进而来管理整个应用的 state

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

这差不多就是 Redux 思想的全部。注意到没我们还没有使用任何 Redux 的 API。Redux 里有一些工具来简化这种模式,但是主要的想法是如何根据这些 action 对象来更新 state,而且 90% 的代码都是纯 JavaScript,没用 Redux、Redux API 和其它魔法。

联系与总结

步骤如下:

  1. 编写action对象
  2. 使用dispath函数发起action
  3. 使用reducers处理被发起的action( 函数内部可能会修改store,然后返回store的最新状态,也就是说state)

三大原则

单一数据源

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console.log(store.getState())

/* 输出
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
*/

State 是只读的

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

这样确保了视图和网络请求都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行

1
2
3
4
5
6
7
8
9
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})

store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})

使用纯函数来执行修改

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

纯函数:是指不依赖于 且 不改变 它作用域之外的变量状态的函数。也就是说,纯函数的返回值只由它调用时的参数决定,它的执行不依赖于系统的状态(比如:何时、何处调用它。

纯函数的特点:

  • 给定相同的输入,将始终返回相同的输出。
  • 无副作用。意味着它无法更改任何外部状态。
1
2
3
4
5
6
7
8
// 不纯的 addToCart 函数改变了现有的 cart 对象
const addToCart = (cart, item, quantity) => {
cart.items.push({
item,
quantity
});
return cart;
};
  • Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。
  • 刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分,因为 reducer 只是函数,你可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 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
36
function visibilityFilter(state = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}

function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}

import { combineReducers, createStore } from 'redux'
let reducer = combineReducers({ visibilityFilter, todos })
let store = createStore(reducer)

快速入门

Action

  • Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。Action 本质上是 JavaScript 普通对象。
  • 一般来说会通过 store.dispatch() 将 action 传到 store。

添加新 todo 任务的 action 是这样的:

1
2
3
4
5
6
7
8
9
const ADD_TODO = 'ADD_TODO'

// 使用字符串类型的 type 字段来表示将要执行的动作
// 多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。
// 除了 type 字段外,action 对象的结构完全由你自己决定。
{
type: ADD_TODO,
text: 'Build my first Redux app'
}

Action 创建函数

Action 创建函数 就是生成 action 的方法。这样做将使 action 创建函数更容易被移植和测试。

1
2
3
4
5
6
7
8
9
10
11
12
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}

// 只需把 action 创建函数的结果传给 dispatch() 方法即可发起一次 dispatch 过程。
dispatch(addTodo(text))
// 或者创建一个 被绑定的 action 创建函数 来自动 dispatch:
const boundAddTodo = text => dispatch(addTodo(text))
boundAddTodo(text);

使用

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
/*
* action 类型
*/

export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

/*
* 其它的常量
*/

export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
}

/*
* action 创建函数
*/

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 }
}

Reducer

  • Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的
  • 记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。

state:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}

Action 处理

reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

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

  1. 修改传入参数;
  2. 执行有副作用的操作,如 API 请求和路由跳转;
  3. 调用非纯函数,如 Date.now()Math.random()
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { VisibilityFilters } from './actions'

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

function todoApp(state, action) {
if (typeof state === 'undefined') {
return initialState
}

// 这里暂不处理任何 action,
// 仅返回传入的 state。
return state
}

// 或者
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
// 使用 Object.assign() 新建了一个副本
// 不能使用 Object.assign(state, { visibilityFilter: action.filter }),因为它会改变第一个参数的值。
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
}
}

拆分 Reducer

上面代码的 todosvisibilityFilter 的更新看起来是相互独立的。我们可以把 todos 更新的业务逻辑拆分到一个单独的函数里:

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
36
37
38
39
40
41
42
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:
return Object.assign({}, state, {
todos: todos(state.todos, action)
})
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: todos(state.todos, action)
})
default:
return state
}
}

能否抽出一个 reducer 来专门管理 visibilityFilter?当然可以:

注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 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
36
37
38
39
40
41
42
43
44
const { SHOW_ALL } = VisibilityFilters

// 负责 todos 字段
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
}
}

// 负责 filter 字段
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}

// 主 reducer 并不需要设置初始化时完整的 state。初始时,如果传入 undefined, 子 reducer 将负责返回它们的默认值。
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}

这就是所谓的reducer 合成,它是开发 Redux 应用最基础的模式。

combineReducers()

combineReducers() 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。没有任何魔法。正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都没有更改 state,那么也就不会创建一个新的对象。

最后,Redux 提供了 combineReducers() 工具类来做上面 todoApp 做的事情,这样就能消灭一些样板代码了。有了它,可以这样重构 todoApp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { combineReducers } from 'redux'
const todoApp = combineReducers({
// 默认调用和key名字相同的函数
visibilityFilter,
todos
})

export default todoApp

// 上面的写法和下面完全等价:
export default function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}

你也可以给它们设置不同的 key,或者调用不同的函数。下面两种合成 reducer 方法完全等价:

1
2
3
4
5
6
7
8
9
10
11
12
13
const reducer = combineReducers({
a: doSomethingWithA,
b: processB,
c: c
})

function reducer(state = {}, action) {
return {
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
}
}

使用

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
36
37
38
39
40
41
42
43
44
45
46
47
48
import { combineReducers } from 'redux'
import {
ADD_TODO,
TOGGLE_TODO,
SET_VISIBILITY_FILTER,
VisibilityFilters
} from './actions'
const { SHOW_ALL } = VisibilityFilters

function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}

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
}
}

const todoApp = combineReducers({
visibilityFilter,
todos
})

export default todoApp

Store

  • 使用 action 来描述“发生了什么”,
  • 使用 reducers 来根据 action 更新 state 的用法。

Store 就是把action和reducers联系到一起的对象。Store 有以下职责:

根据已有的 reducer 来创建 store

1
2
3
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)

发起 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())

// 注意 subscribe() 返回一个函数用来注销监听器
// 每次 state 更新时,打印日志
const 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()

数据流

严格的单向数据流是 Redux 架构的设计核心。

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

Redux 应用中数据的生命周期

  1. 调用 store.dispatch(action)。

    • 你可以在任何地方调用 store.dispatch(action),包括组件中、XHR 回调中、甚至定时器中。
  2. Redux store 调用传入的 reducer 函数。

    • Store 会把两个参数传入 reducer: 当前的 state 树和 action。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      // 当前应用的 state(todos 列表和选中的过滤器)
      let previousState = {
      visibleTodoFilter: 'SHOW_ALL',
      todos: [
      {
      text: 'Read the docs.',
      complete: false
      }
      ]
      }

      // 将要执行的 action(添加一个 todo)
      let action = {
      type: 'ADD_TODO',
      text: 'Understand the flow.'
      }

      // reducer 返回处理后的应用状态
      let nextState = todoApp(previousState, action)
  3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。

    • Redux 原生提供combineReducers()辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      function todos(state = [], action) {
      // 省略处理逻辑...
      return nextState
      }

      function visibleTodoFilter(state = 'SHOW_ALL', action) {
      // 省略处理逻辑...
      return nextState
      }

      let todoApp = combineReducers({
      todos,
      visibleTodoFilter
      })

      当你触发 action 后,combineReducers 返回的 todoApp 会负责调用两个 reducer:

      1
      2
      let nextTodos = todos(state.todos, action)
      let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)

      然后会把两个结果集合并成一个 state 树:

      1
      2
      3
      4
      return {
      todos: nextTodos,
      visibleTodoFilter: nextVisibleTodoFilter
      }
  4. Redux store 保存了根 reducer 返回的完整 state 树。

搭配 React

  • Redux 和 React 搭配起来用很好,因为这类库允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。

实现展示组件

它们只是普通的 React 组件。我们会使用函数式无状态组件,除非需要本地 state 或生命周期函数的场景。这并不是说展示组件必须是函数 – 只是因为这样做容易些。如果你需要使用本地 state,生命周期方法,或者性能优化,可以将它们转成 class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// components/Todo.js
import React from 'react'
import PropTypes from 'prop-types'

const Todo = ({ onClick, completed, text }) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
)

Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}

export default Todo
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
// components/TodoList.js
import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'

const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map((todo, index) => (
<Todo key={index} {...todo} onClick={() => onTodoClick(index)} />
))}
</ul>
)

TodoList.propTypes = {
todos: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired
).isRequired,
onTodoClick: PropTypes.func.isRequired
}

export default TodoList
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
// components/Link.js
import React from 'react'
import PropTypes from 'prop-types'

const Link = ({ active, children, onClick }) => {
if (active) {
return <span>{children}</span>
}

return (
<a
href=""
onClick={e => {
e.preventDefault()
onClick()
}}
>
{children}
</a>
)
}

Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}

export default Link
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// components/Footer.js
import React from 'react'
import FilterLink from '../containers/FilterLink'

const Footer = () => (
<p>
Show: <FilterLink filter="SHOW_ALL">All</FilterLink>
{', '}
<FilterLink filter="SHOW_ACTIVE">Active</FilterLink>
{', '}
<FilterLink filter="SHOW_COMPLETED">Completed</FilterLink>
</p>
)

export default Footer

实现容器组件

  • 现在来创建一些容器组件把这些展示组件和 Redux 关联起来。
  • 技术上讲,容器组件就是使用 store.subscribe() 从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。
  • 你可以手工来开发容器组件,但建议使用 React Redux 库的 connect() 方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。
  • 使用 connect() 前,需要先定义 mapStateToProps 这个函数来指定如何把当前 Redux store state 映射到展示组件的 props 中。例如,VisibleTodoList 需要计算传到 TodoList 中的 todos,所以定义了根据 state.visibilityFilter 来过滤 state.todos 的方法,并在 mapStateToProps 中使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
case 'SHOW_ALL':
default:
return todos
}
}

const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}

除了读取 state,容器组件还能分发 action。类似的方式,可以定义 mapDispatchToProps() 方法接收 dispatch() 方法并返回期望注入到展示组件的 props 中的回调方法。例如,我们希望 VisibleTodoListTodoList 组件中注入一个叫 onTodoClick 的 props ,还希望 onTodoClick 能分发 TOGGLE_TODO 这个 action:

1
2
3
4
5
6
7
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}

最后,使用 connect() 创建 VisibleTodoList,并传入这两个函数。

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

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

export default VisibleTodoList

这就是 React Redux API 的基础。

其它容器组件定义如下:

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
// containers/FilterLink.js
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}

const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter))
}
}
}

const FilterLink = connect(
mapStateToProps,
mapDispatchToProps
)(Link)

export default FilterLink
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
36
// containers/VisibleTodoList.js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

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)
}
}

const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}

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

export default VisibleTodoList

其它组件

AddTodo 组件的视图和逻辑混合在一个单独的定义之中。

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
// containers/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'

let AddTodo = ({ dispatch }) => {
let input

return (
<div>
<form
onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}
>
<input
ref={node => {
input = node
}}
/>
<button type="submit">Add Todo</button>
</form>
</div>
)
}
AddTodo = connect()(AddTodo)

export default AddTodo

将容器放到一个组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// components/App.js
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'

const App = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
)

export default App

传入 Store

所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store 把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了一个容器组件。

建议的方式是使用指定的 React Redux 组件<Provider>魔法般的 让所有容器组件都可以访问 store,而不必显式地传递它。只需要在渲染根组件时使用即可。

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

let store = createStore(todoApp)

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

示例: Todo 列表

入口文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// index.js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import rootReducer from './reducers'
import App from './components/App'

const store = createStore(rootReducer)

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

创建 Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// actions/index.js
let nextTodoId = 0
export const addTodo = text => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
})

export const setVisibilityFilter = filter => ({
type: 'SET_VISIBILITY_FILTER',
filter
})

export const toggleTodo = id => ({
type: 'TOGGLE_TODO',
id
})

export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
}

Reducers

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

export default todos
1
2
3
4
5
6
7
8
9
10
11
// reducers/visibilityFilter.js
const visibilityFilter = (state = 'SHOW_ALL', action) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}

export default visibilityFilter
1
2
3
4
5
6
7
8
9
// reducers/index.js
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'

export default combineReducers({
todos,
visibilityFilter
})

展示组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// components/Todo.js
import React from 'react'
import PropTypes from 'prop-types'

const Todo = ({ onClick, completed, text }) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
)

Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}

export default Todo
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
// components/TodoList.js
import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'

const TodoList = ({ todos, toggleTodo }) => (
<ul>
{todos.map(todo => (
<Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
))}
</ul>
)

TodoList.propTypes = {
todos: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired
).isRequired,
toggleTodo: PropTypes.func.isRequired
}

export default TodoList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// components/Link.js
import React from 'react'
import PropTypes from 'prop-types'

const Link = ({ active, children, onClick }) => (
<button
onClick={onClick}
disabled={active}
style={{
marginLeft: '4px'
}}
>
{children}
</button>
)

Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}

export default Link
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// components/Footer.js
import React from 'react'
import FilterLink from '../containers/FilterLink'
import { VisibilityFilters } from '../actions'

const Footer = () => (
<div>
<span>Show: </span>
<FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink>
</div>
)

export default Footer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// components/App.js
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'

const App = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
)

export default App

容器组件

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
// containers/VisibleTodoList.js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
case 'SHOW_ALL':
default:
return todos
}
}

const mapStateToProps = state => ({
todos: getVisibleTodos(state.todos, state.visibilityFilter)
})

const mapDispatchToProps = dispatch => ({
toggleTodo: id => dispatch(toggleTodo(id))
})

export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// containers/FilterLink.js
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => ({
active: ownProps.filter === state.visibilityFilter
})

const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
})

export default connect(
mapStateToProps,
mapDispatchToProps
)(Link)

其他组件

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
// containers/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'

const AddTodo = ({ dispatch }) => {
let input

return (
<div>
<form
onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}
>
<input ref={node => (input = node)} />
<button type="submit">Add Todo</button>
</form>
</div>
)
}

export default connect()(AddTodo)