React 升级:Redux

前言

近期接触React项目,学到许多新知识点,网上教程甚多,但大多都把知识点分开来说,初学者容易陷入学习的误区,摸不着头脑,本人在学习中也遇到许多坑。此篇文章是笔者看过的写得比较详细的具体的,同时能把全部的知识点统一串联起来,很是适合初学者学习。因为文档是英文版,考虑到大伙英语水平各不相同,故作这次翻译,一来深化本身对Redux的体系认知,二来方便你们理解阅读。javascript

因为文中出现大量技术名词,应适当结合原文进行阅读,原文链接:
《Leveling Up with React: Redux》 By Brad Westfall On March 28, 2016css

此篇教程是原文做者一系列教程的最后一篇,这里只对该篇进行翻译,剩余的几篇有时间会继续进行翻译,对于文中出现的翻译错误,欢迎你们积极指正。html


本教程是 Brad Westfall 三部分系列教程的最后一篇。咱们将学习如何有效地管理状态,使其跨越整个应用程序,而且能够在没有严重复杂度的状况下进行衡量。在React的学习道路上咱们已经走了这么远,如今是时候来跨过终点,得到这个物超所值的所有成长历程。java

系列文章

Redux 是一个用来管理JavaScript应用中 data-state(数据状态)和UI-state(UI状态)的工具,对于那些随着时间推移状态管理变得愈来愈复杂的单页面应用(SPAs)它是比较理想的,同时,它又是和框架无关的,所以,尽管它是提供给React使用的,但它也能够结合Angular 或者 jQuery来使用。git

另外,它的设想来自一个叫作“时间旅行”的实验,这是真实的,咱们后面会讲到。es6

正如咱们前面的教程所提到的,React 在组件之间流通数据.更确切的说,这被叫作“单向数据流”——数据沿着一个方向从父组件流到子组件。因为这个特性,对于没有父子关系的两个组件之间的数据交流就变得不是那么显而易见。github

React 不推荐组件对组件直接交流的这种方式,尽管它确实有一些特征能够支持这个方法,但在许多组件之间进行直接的组件对组件的交流被认为是很差的作法,由于这样会容易出错,而且致使spaghetti code —— 过期的代码, 很难维护。

React 提供了一个建议,可是他们但愿你能本身来实现它。这里是React官方文档里的一段话:

想让两个没有父子关系的组件进行交流,你能够经过设置你本身的全局事件机制…… Flux 模式就是其中一个可行的方案

这里 Redux 就排上用场了。Redux提供了一个解决方案,经过将应用程序全部的状态都存储在一个地方,叫作“store”。而后组件就能够“dispatch”状态的改变给这个store,而不是直接跟另外的组件交流。全部的组件都应该意识到状态的改变能够“subscribe”给store。

能够把store想象成是应用程序中全部状态改变的中介。随着Redux的介入,全部的组件再也不相互直接交流,而是全部的状态改变必须经过store这个单一的真实来源。

这和那些应用程序中不一样的部分直接交流的策略有很大的不一样。有时,那些策略被认为是容易出错和混乱的缘由:

有了Redux,全部的组件都从store中来获取他们的状态,变得很是清晰。一样,组件状态的改变都发送给了store,也很清晰。组件初始化状态的改变只须要关心如何派发给store,而不用去关心一系列其它的组件是否须要状态的改变。这就是Redux如何使数据流变得更简单的缘由。

使用store来协调应用之间状态改变的概念就是Flux模式。它是一种倾向单向数据流(好比 React)的设计模式。Redux像Flux,可是他们又有多少关系呢?

Redux is "Flux-like"

Flux 是一种模式,不像Redux那样是能够下载的工具,Redux 是受Flux模式,此外,它比较像Elm。这里有许多有关于Redux和Flux之间比较的指南。它们中的大多数都会得出Redux就是Flux,或者Redux和Flux比较相似的结论,这取决于给Flux定义的规则到底有多严格。然而说到底,这些都可有可无。Facebook 很是喜欢而且支持Redux,这从它们雇佣了Redux的主要开发者 Dan Abramov 就能够看出。

这篇文章假设你一点都不熟悉Flux的设计模式。不过若是你熟悉,你会注意到许多微小的不一样,尤为考虑到Redux的三大指导原则

1. 单一真实源

Redux只使用一个store来处理应用的状态。由于全部的状态都驻留在同一个地方,Redux称这个为单一真实源。

store中数据的结构彻底取决于你,但一般都是针对应用的一个深层嵌套的对象。

Redux的单一store方法是区分Flux多个store方法的最主要区别。

2. 状态是只读的

Redux的文档指出,惟一改变状态的方法就是发出一个action,一个用来描述发生了什么的对象。

这意味着应用不能直接改变状态,相反,“actions” 被派发给store,用来描述一个改变状态的意图。

store对象本身有几个小型的API,对应4个方法:

  • store.dispatch(action)

  • store.subscribe(listener)

  • store.getState()

  • replaceReducer(nextReducer)

因此你能够看到,这里没有设置状态的方法。所以,派发一个action是处理应用状态更改的惟一办法

var action = {
  type: 'ADD_USER',
  user: {name: 'Dan'}
};

// Assuming a store object has been created already
store.dispatch(action);

dispatch() 方法发送了一个对象给Redux,这个对象就被叫作action。这个action能够被描述成一个携带了一个 type 属性以及其它能够被用来更新状态的数据(在这个例子里就是user)的有效负载。记住,在 type 属性以后,这个action对象的设计彻底取决于你。

3. 全部的状态改变使用的都是纯函数

就像刚才所描述的,Redux不容许应用直接改变状态,而是用被分派的action来“描述”状态改变或者改变状态的意图。而一个个Reducer就是你本身写的函数,用来处理分派的action,事实上是它真正改变了状态。

一个reducer接受当前的状态(state)做为参数,并且必须返回一个新的状态才能改变以前的状态。

// Reducer Function
var someReducer = function(state, action) {
  ...
  return state;
}

reducer 必须使用 “纯”函数 , 一个能够用如下这些特征来描述的术语:

  • 没有任何的网络或数据库请求操做

  • 返回的值仅依赖于参数

  • 参数必须是“不可改变的”,觉得着它们将不能被更改。

  • 调用具备相同参数集的纯函数将始终返回相同的值

它们被称为“纯”函数是由于它们什么都不作仅仅返回一个基于参数的值。它们在系统的任何其余部分都没有反作用。

第一个 Redux Store

开始以前,须要先用 Redux.createStore() 建立一个store,而后将全部的reducer做为参数传递进去,让咱们看一下这个只传递了一个reducer的小例子:

// Note that using .push() in this way isn't the
// best approach. It's just the easiest to show
// for this example. We'll explain why in the next section.

// The Reducer Function
var userReducer = function(state, action) {
  if (state === undefined) {
    state = [];
  }
  if (action.type === 'ADD_USER') {
    state.push(action.user);
  }
  return state;
}

// Create a store by passing in the reducer
var store = Redux.createStore(userReducer);

// Dispatch our first action to express an intent to change the state
store.dispatch({
  type: 'ADD_USER',
  user: {name: 'Dan'}
});

上面的程序干了些什么呢:

  1. 这个store只由一个reducer建立。

  2. 这个reducer 初始化状态的时候使用了一个空数组 。*

  3. 在被分派的这个action里面使用了新的user对象。

  4. 这个reducer将这个新的user对象附加到state上,并将它返回,用来更新store。

*在这个例子里reducer实际上被调用了两次 —— 一次是在建立store的时候,一次是在分派action以后。

当store被建立以后,Redux当即调用了全部的reducer,而且将它们的返回值做为初始状态。第一次调用reducer传递了一个 undefined 给state。通过reducer内部的代码处理以后返回了一个空数组给这个store的state做为开始。

全部的reducer在每次action被分派以后都会被调用。由于reducer返回的状态将会成为新的状态存储在store中,因此 Redux老是但愿全部的reducer都要返回一个状态

在这个例子中,reducer第二次的调用发生在分派以后。记住,一个被分派的action描述了一个改变状态的意图,并且一般携带有数据用来更新状态。这一次,Redux将当前的状态(仍旧是空数组)和action对象一块儿传递给了reducer。这个action对象,如今有了一个值为‘ADD_USER’的type属性, 让reducer知道怎样改变状态。

咱们很容易就能将reducers和漏斗联想起来,容许状态经过他们。这是由于reducers老是接受和返回状态用来更新store。

基于这个例子,咱们的store将会变成一个只有一个user对象的数组:

store.getState();   // => [{name: 'Dan'}]

不要改变状态,复制它

在咱们上面的例子中这个reducer从技术上来说是可行的,可是它改变了状态,这是一种很差的作法。尽管reducers 负责改变状态,可是不该该直接改变“现有的状态”。因此咱们不该该在reducer的state这个参数上使用.push()这个变异的方法

传递给reducer的参数应该被视为不可改变的。换句话说,他们不该该被直接改变。咱们可使用不变异的方法好比.concat()来拷贝这个数组,而后咱们将拷贝的数组返回。

var userReducer = function(state = [], action) {
  if (action.type === 'ADD_USER') {
    var newState = state.concat([action.user]);
    return newState;
  }
  return state;
}

在这个新的reducer中,咱们添加了一个新的user对象做为state参数的副本被改变和返回。当没有添加新的用户的时候,注意返回的是原始的state而不是它的拷贝。

有一大节关于不可变数据结构的最佳尝试,咱们应该更多的去了解

你也许已经注意到初始化参数使用了ES2015的默认参数方法。到目前为止,在这一些列的文章中,咱们一直避免使用ES2015来使你更专心于主题内容。然而,Redux和ES2015结合使用会变得很是完美。所以,咱们最终开始在这篇文章中使用ES2015。然而不用担忧,每次采用新的ES2015的特性,咱们都会指出来而且解释

多个reducer

上一个例子是一个很好的入门,可是大多数的应用都须要更复杂的state来知足整个应用。由于Redux仅使用一个store,因此咱们须要使用嵌套的对象来组织不一样模块的state。假设咱们的想要咱们的store相似于这种样子:

{
  userState: { ... },
  widgetState: { ... }
}

整个应用对应的仍是 “一个store = 一个对象”,可是它嵌套了 userStatewidgetState 对象,能够包含各类数据。这彷佛过于简单了,可是实际上和一个真实的Redux store没多少差异。

为了建立具备嵌套对象的store,咱们须要定义每一块的reducer:

import { createStore, combineReducers } from 'redux';

// The User Reducer
const userReducer = function(state = {}, action) {
  return state;
}

// The Widget Reducer
const widgetReducer = function(state = {}, action) {
  return state;
}

// Combine Reducers
const reducers = combineReducers({
  userState: userReducer,
  widgetState: widgetReducer
});

const store = createStore(reducers);

ES2015 提示! 在这个例子中四个主要的变量都不会被改变, 因此咱们将它们定义成常量. 同时咱们也使用了ES2015 modules and destructuring

combineReducers()容许咱们用不一样的逻辑块来描述store,将reducer分配给每个块。如今,每个reducer返回的初始状态会进入到它们store中各自对应的userState或者widgState块。

有些很是重要的点须要注意,如今每个reducer中所传递的只是所有状态中各自的部分,再也不像以前只有一个reducer时传递的是整个store的状态。而后每一个reducer返回的状态应用于它们各自的部分。

在分派以后调用的是哪个Reducer?

当咱们考虑每次action被分派的时候,把上面所有的reducer想一想成一个个漏斗会变得更加明了,全部的reducer都会被调用,都将有机会来更新各自的状态:

我很当心地说“它们的”状态是由于reducer的“当前状态”参数和它的返回“更新”状态仅仅影响到store中reducer里面的部分。记住,像前面所说的,每个reducer只得到它们各自的状态,而不是整个状态。

Action 策略

实际上有大量的关于建立和管理action及其类型的策略。虽然它们都很棒,可是它们不像本文中的其余一些信息那样重要。为了减小文章的篇幅,咱们整理了这些基本的action策略,你能够在 GitHub repo上得到这一系列的策略。

不可变的数据结构

state的样式由你本身决定: 它能够是原始值,数组,对象,或者一个Immutable.js的数据结构。 惟一重要的部分就是你不能改变state对象,并且须要返回一个更改后的新对象 -- Redux 文档

上面的陈诉说了不少,咱们已经在本教程中提到了这一点。若是咱们开始讨论什么是可变的什么是不可变的的前因后果和利弊,咱们能够在 《blog article's worth of information》找到更有价值的信息。因此事实上,我只是想突出一些要点。

开始前:

  • JavaScript的原始数据类型(Number, String, Boolean, Undefined, and Null) 已是不可变得了。

  • 对象、数组、函数是可变的。

有人说数据结构的可变性容易产生问题。由于咱们的store是有state对象和数组所组成,咱们须要实施一种策略来保持状态不可变。

让咱们假设须要改变一个state对象的属性,这里有三种方式:

// Example One
state.foo = '123';

// Example Two
Object.assign(state, { foo: 123 });

// Example Three
var newState = Object.assign({}, state, { foo: 123 });

第一个和第二个例子都改变了state对象。第二个例子是由于Object.assign()把全部的参数都合并到了第一个参数里。但这也就是为何第三个例子没有改变state对象的缘由。

第三个例子将state的内容和{foo: 123}合并到了一个新的空对象中。这是一种常见的技巧,容许咱们建立一个state对象的副本,在副本上进行修改,本质上不会影响原始的state

对象的“扩展运算符”是保持state不可变的另外一种方式:

const newState = { ...state, foo: 123 };

有关于上述代码究竟发生了什么,为何它对Redux是友好的详细解释,能够参考这个主题的文档

Object.assign() 和扩展运算符都是ES2015的特性。

总结来讲,有许多方法能够明确地保持对象和数组不可变。许多开发者使用第三方库好比 seamless-immutableMori 甚至Facebook本身的Immutable.js 来达到这个目的。

我很是当心的选择了一些相关的博客和教程。若是你不是很是明白不变性,能够看一下上面给出的这些连接。这在Redux的学习中是一个很是重要的概念。

初始化状态 和 时间旅行

若是你读过文档,你也许会注意到createStore()这个方法里的第二个参数是用来“初始化状态”的,这也许是对reducer建立初始化状态方式的一种替代。然而,这个初始化的状态只会被用来“state hydration”。

想象一下一个用户刷新了你的单页面应用,store中的状态被重置为reducer中的初始状态,这样多是不理想的。

相反,想象一个你可使用一种策略来保持store,而后在刷新的时候从新将它化合到Redux中。这就是传送一个初始化状态到createStore()中的缘由。

这带来了一个有趣的概念,若是从新化合老的状态变得这么容易,咱们能够将app中的状态想象成是时间旅行。这能够被用来进行调试或者撤销/重作某些特性。因此将全部的状态存储在一个store中变得颇有意义。这就是为何不可变的状态可以帮助咱们的其中一个缘由。

在一次面谈中,Dan Abramov 被问到“为何你要开发Redux?”

我并非有意要建立Flux框架。当React第一次被宣布的时候,我提议来谈一谈‘热加载和时间旅行’,可是老实说,我本身也不知道该怎么实施时间旅行

Redux with React

就像咱们已经讨论过的,Redux与框架无关。在咱们开始考虑Redux跟React怎么结合以前,明白Redux的核心概念是很是重要的。可是如今咱们已经准备好从上一篇文章中拿一个容器组件,而后将Redux应用在它上面了。

首先,这是没有使用Redux的原始组件代码:

import React from 'react';
import axios from 'axios';
import UserList from '../views/list-user';

const UserListContainer = React.createClass({
  getInitialState: function() {
    return {
      users: []
    };
  },

  componentDidMount: function() {
    axios.get('/path/to/user-api').then(response => {
      this.setState({users: response.data});
    });
  },

  render: function() {
    return <UserList users={this.state.users} />;
  }
});

export default UserListContainer;

ES2015 说明!这个例子已经在原始代码的基础上作了部分转换,使用了ES2015的模块功能和箭头函数。

它所作的就是发送一个Ajax请求,而后更新它的本地状态。可是,若是该应用的其它区域也要根据这个新获取到的用户列表进行改变呢,这个策略是不够的。

有了Redux策略,咱们能够在Ajax请求的时候分派一个action而不是进行 this.setState(),而后这个组件和其它组件能够订阅状态的改变。可是事实上这带给咱们一个问题,咱们应该怎么设置store.subscribe()来更新组件的状态呢?

我想我能够提供几个例子来手动的链接一些组件到Redux store。你也能够想象一下用你的方法会怎么作。可是最终,在这些例子的最后我会解释有一个更好的办法,而后忘掉这些手动的例子。而后我会介绍官方的链接React和Redux的模块,叫作react-redux,因此仍是直接跳到那一步吧。

使用 react-redux 进行链接

为了说明白,reactreduxreact-redux是npm上三个独立的模块。其中,react-redux模块容许咱们以更方便的方式“connect” React组件和Redux

下面给出例子:

import React from 'react';
import { connect } from 'react-redux';
import store from '../path/to/store';
import axios from 'axios';
import UserList from '../views/list-user';

const UserListContainer = React.createClass({
  componentDidMount: function() {
    axios.get('/path/to/user-api').then(response => {
      store.dispatch({
        type: 'USER_LIST_SUCCESS',
        users: response.data
      });
    });
  },

  render: function() {
    return <UserList users={this.props.users} />;
  }
});

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

export default connect(mapStateToProps)(UserListContainer);

这里面有许多的新东西:

一、咱们从 react-redux 中引入了 connect 函数。
二、这段代码可能从最底下的链接操做开始往上看会更容易理解。connect()方法实际上接收两个参数,可是咱们这里只显示了一个 mapStateToProps()

connect()() 多了一个括号看起来好像很奇怪,实际上这是两个函数的调用。首先,connect()返回了另一个函数,我想咱们能够把这个函数赋值给一个变量名,而后调用它,可是既然在后面多加一个括号就能够直接调用这个函数,咱们为何还要给它设置一个函数名呢?并且,在这个函数调用结束以后,咱们根本不须要这个额外的函数名。这第二个函数须要你传递一个React组件。在这个例子中,传递的是咱们的容器组件。我敢打赌你确定正在思考“为何要把它变得这么复杂?”,然而,这其实是一种常见的“函数式编程”范式,因此,学习如何使用它是很是有好处的。

三、connect()第一个参数是须要返回一个对象的函数。这个对象的属性会成为这个组件的“props”。你能够看到它们的状态值。如今,我但愿“mapStateToProps”变得更有意义。同时,咱们也看到mapStateToProps()这个函数接收了一个参数,这个参数就是整个Redux的store。mapStateToProps()函数的主体思想就是将这个组件须要用到的部分状态从所有状态中隔离出来做为它的props属性。

四、根据第3点中所说的,咱们将再也不须要getInitialState()的存在。同时,咱们也看到,自从users这个数组变成了props属性而不是本地组件状态以后,咱们参考使用this.props.users而不是this.state.users

五、Ajax的返回如今变成了一个action的分派,而不是本地状态的更新。为了更简单明了的展现,咱们没有使用action构造器和action type常量

下面的代码提供了一种在用户自定义的reducer没有出现的时候也能够工做的假设。注意store的userState属性,可是这个名字是哪里来的呢?

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

这个名字来自咱们合并全部的reducer的时候:

const reducers = combineReducers({
  userState: userReducer,
  widgetState: widgetReducer
});

userState.users属性又是什么?它又来自哪里?

在这个例子中,咱们并无展现一个实际的reducer(由于它会出如今另外一个文件中),reducer决定了它所负责状态的子属性。为了确保.usersuserState的一个属性,上述例子对应的reducer可能看起来是这样的:

const initialUserState = {
  users: []
}

const userReducer = function(state = initialUserState, action) {
  switch(action.type) {
  case 'USER_LIST_SUCCESS':
    return Object.assign({}, state, { users: action.users });
  }
  return state;
}

在 Ajax 不一样生命周期进行分派

在咱们Ajax的例子中,咱们仅仅分派了一个action。它被特地叫作“USER_LIST_SUCCESS”,由于咱们同时也但愿在Ajax调用开始的时候分派一个“USER_LIST_REQUEST”的action,在Ajax调用失败的时候分派一个“USER_LIST_FAILED”的action。请确保读取异步操做的文档

分派事件

在以前的文章中,咱们看到事件应该经过容器组件传递到表现组件。原来 react-redux同时也能够处理这个,一个事件只须要分派一个action:

...

const mapDispatchToProps = function(dispatch, ownProps) {
  return {
    toggleActive: function() {
      dispatch({ ... });
    }
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(UserListContainer);

在表现组件中,就像咱们以前作过的,能够经过onClick={this.props.toggleActive}来调用事件,不须要再编写事件自己。

容器组件省略

有时,一个容器组件只须要订阅store,不须要任何像componentDidMount()这样的方法来开始Ajax 请求。它只须要一个render()方法传递给表现组件。在这个例子中,咱们能够像这样构造容器组件:

import React from 'react';
import { connect } from 'react-redux';
import UserList from '../views/list-user';

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

export default connect(mapStateToProps)(UserList);

是的,父老乡亲们,这就是新的容器组件的整个文件。可是等一下,容器组件在哪里?为何咱们在这里没有用到任何的React.createClass()

事实证实,connect()方法为咱们构造了一个容器组件。注意到这一次咱们直接传递的是一个表现组件,而不是咱们本身建立的容器组件。若是你真的在想容器组件干了什么,记住,它们的存在是为了表现组件专心于视图,而不是状态。它们也传递状态给子视图做为props。而这就是connect()实际所作的,它传递了状态(做为props)给咱们的表现组件,而后返回一个React组件来包裹这个表现组件。从本质上来讲,这个包裹,就是容器组件。

因此是否是意味着上面的例子中其实有两个容器组件包裹着一个表现组件?固然,你能够这样子认为。但这并无什么问题,只有当咱们的容器组件须要除了render()方法以外的其它方法的时候它才是必须的。

想象这两个容器组件是具备不一样可是相关服务的角色:

嗯,也许这就是为何React的logo看起来这么像原子的缘由吧

Provider

为了保证任何react-redux的代码能正常工做,你须要使用一个<Provider />组件来让你的应用知道怎样使用react-redux。这个组件包裹了你的整个React应用。若是你正在使用 React Router,它看起来也许是这样的:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import router from './router';

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

经过react-redux真正“链接”React和Redux的东西是附加给Provider的store,这里有个例子,关于主要入口点大概是怎么样

Redux with React Router

这个不作要求,可是有另外一个npm项目叫作 react-router-redux ,由于从技术上来讲,路由是UI-state的一部分,并且React Router不认识Redux,因此这个项目帮助咱们链接这两个东西。

你看到我作了什么吗?咱们走了一圈,又回到了第一篇文章!

项目最后

遵守这一系列教程,最终你能够实现一个叫作“用户控件”的单页面应用。

与本系列其余文章同样,每一个都有相关指导文档,在Github上也都有相关代码指导你怎么作。

总结

我真的但愿你能喜欢我写的这一系列文章,我意识到有许多关于React的主题咱们都没有覆盖到,但我试图在保持真实的前提下,给新用户一种跨越React基础知识的认知,以及制做一个单页面应用所带来的感觉。


系列文章


翻译文献Leveling Up with React: Redux By Brad Westfall On March 28, 2016

翻译做者:coocier

相关文章
相关标签/搜索