[译] Redux 的工做过程

Redux 的工做过程: 一个计数器例子

在学习了一些 React 后开始学习 Redux,Redux 的工做过程让人感到很困惑。javascript

Actions,reducers,action creators(Action 建立函数),middleware(中间件),pure functions(纯函数),immutability(不变性)…前端

这些术语看起来很是陌生。java

因此在这篇文章中我将用一种有利于你们理解的反向剖析的方法去揭开 Redux 怎样工做的神秘面纱。在 上一篇 中,在提出专业术语以前我将尝试用简单易懂的语言去解释 Redux。react

若是你还不明确 Redux 是干什么的 或者为何要使用它,请先移步 这篇文章 而后再回到这里继续阅读。android

第一:明白 React 的状态 state

咱们将从一个简单的使用 React 状态的例子开始,而后一点一点地添加Redux。ios

这是一个计数器:git

计数器组件

这里是代码 (为了使代码简单我没有贴出 CSS 代码,因此下面代码的效果会不会像上面图片同样美观):github

import React from 'react';

class Counter extends React.Component {
  state = { count: 0 }

  increment = () => {
    this.setState({
      count: this.state.count + 1
    });
  }

  decrement = () => {
    this.setState({
      count: this.state.count - 1
    });
  }

  render() {
    return (
      <div>
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span>{this.state.count}</span>
          <button onClick={this.increment}>+</button>
        </div>
      </div>
    )
  }
}

export default Counter;
复制代码

简单的看一下他是怎样跑起来的:npm

  • 这个 count 状态被存储在最外层组件 Counter 里面
  • 当用户点击 “+”,这个按钮的 onClick 回调函数被触发, 也就是组件 Counter 里面的 increment 方法被调用。
  • increment 方法用新的数字更新状态 count。
  • 因为状态被改变了, React 从新渲染 Counter 组件 (还有它的子组件), 而后显示新的计数器的值.

若是你想要了解更多的状态怎么被改变的细节,去阅读 React 中状态的图形化指南 而后再回到这里。严格来说:若是上面的例子没有帮助你回顾起 React 的 state ,那么在你学习 Redux 以前应该去学习 React 的 state 是怎么工做的。编程

快速开始

若是你想经过代码学习,如今就建立一个项目:

  • 若是你以前没有安装 create-react-app ,那么先安装 (npm install -g create-react-app)
  • 建立一个项目: create-react-app redux-intro
  • 打开 src/index.js 而后用下面的代码进行替换:
import React from 'react';
import { render } from 'react-dom';
import Counter from './Counter';

const App = () => (
  <div>
    <Counter />
  </div>
);

render(<App />, document.getElementById('root'));
复制代码
  • 用上面的计数器代码建立一个 src/Counter.js

如今: 添加 Redux

第一部分中讨论到,Redux 保存应用程序的状态 state 在单一的状态树 store中。而后你能够将 state 的部分抽离出来,而后以 props 的方式传入组件。这使你能够把数据保存在一个全局的位置(状态树 store )而后将其注入到应用程序中的任何一个组件中,而不用经过多层级的属性传递。

注意:你可能常常看到 “state” 和 “store” 混着使用,可是严格来说: state是数据,而 store 是数据保存的地方。

咱们接着往下走,利用你的编辑器继续编辑咱们下面的代码,它将帮助你理解 Redux 怎么工做(咱们经过讲解一些错误来继续)。

添加 Redux 到你的项目中:

$ yarn add redux react-redux
复制代码

redux vs react-redux

等等 — 这是两个库吗?你可能会问 “react-redux 是什么”?对不起,我一直在骗你。

你看,redux 给了你一个状态树 store,让你能够把状态 state 存在里面,而后能够把状态取出来,当状态改变的时候能够作出响应。然而这是他它作的全部事。实际上正是 react-redux 将 state 与 React 组件联系起来。实际上:redux 和 React 一点儿也没有关系。

这些库就像豌豆荚里面的两粒豌豆,99.999% 的时候当有人在 React 的背景下提到 “Redux” 的时候,他们指的是这两个库。因此记住:当你在 StackOverflow 或者 Reddit 或者其它任何地方看到 Redux 时,他指的是这两个库。

最后一件事

大多数教程一开始就建立一个 store 状态树,设置 Redux,写一个 reducer,等等,出如今屏幕上的任何效果在展示出来以前都会通过大量的操做。

我将采用一种反向推导的方法,使用一样多的代码展示出一样的效果。可是但愿每个步骤后面的原理都能展示地更加清楚。

回到计数器的应用程序,咱们把组件的状态转移到 Redux。

咱们把状态从组件里面移除,由于咱们很快能够从 Redux 中获取它们:

import React from 'react';

class Counter extends React.Component {
  increment = () => {
    // 后面填充
  }

  decrement = () => {
    // 后面填充
  }

  render() {
    return (
      <div>
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span>{this.props.count}</span>
          <button onClick={this.increment}>+</button>
        </div>
      </div>
    )
  }
}

export default Counter;
复制代码

计数器的流程

咱们注意到 {this.state.count} 改变成了 {this.props.count}。固然这不会起做用,由于计数器组件尚未接受 count 属性,咱们经过 Redux 注入这个属性。

为了从 Redux 中得到状态 count,咱们须要在模块的顶部导入 connect 方法:

import { connect } from 'react-redux';
复制代码

而后接下来咱们须要 “connect” 计数器组件到 Redux 中:

// 添加这个函数:
function mapStateToProps(state) {
  return {
    count: state.count
  };
}

// 而后这样替换:
// 默认导出计数器组件;

// 这样导出:
export default connect(mapStateToProps)(Counter);
复制代码

这将发生错误 (在第二部分会有更多错误)。

之前咱们导出函数自己,如今咱们把它用 connect 函数包装后调用。

什么是 connect

你可能注意到这个函数调用看起来有一些奇怪。为何是 connect(mapStateToProps)(Counter) 而不是 connect(mapStateToProps, Counter) 或者 connect(Counter, mapStateToProps)?这将发生什么呢?

之因此这样写是由于 connect 是一个高阶函数,当你调用它的时候会返回一个函数,而后用一个组件作参数调用那个函数返回一个新的包装过的组件。

返回的组件另外一个名字叫作高阶组件 (又叫作 “HOC”)。高阶组件被指责有不少的缺点,可是他们仍然很是有用,connect 就是一个很好的例子。

connect 链接整个状态到了Redux,经过你本身提供的 mapStateToProps 函数, 这须要一个自定义的函数由于只有你本身知道状态在 Redux 中的模型。

connect 链接了全部的状态,“嘿,告诉我你须要从混乱的状态中获得什么”。

mapStateToProps 函数中返回的状态做为属性注入到你的组件中。上面例子中的 state.count 做为 count 属性:对象中的键名做为属性名,它们对应的值做为属性的值。因此你看,从函数的字面意思上是定义了状态到属性的映射

错误意味着有进展!

代码进行到这里,你会在控制台里面看到下面的错误:

Could not find “store” in either the context or props of “Connect(Counter)”. Either wrap the root component in a , or explicitly pass "store" as a prop to "Connect(Counter)".

由于 connect 从 Redux store 树里面获取状态,而咱们尚未建立状态树或者说告诉 app 怎样去找到 store 树,这是一个合乎逻辑的错误,Redux 还不知道如今发生了什么事。

提供一个状态树 store

Redux 控制着整个 app 的所有状态,经过 react-redux 里面的 Provider 组件包裹着整个 app,app 里面的每个组件均可以经过 connect 去进入到 Redux store 里面获取状态。

这意味着最外围的 App 组件,以及 App 的子组件(像 Counter),甚至他们子组件的子组件等等,全部的组件均可以访问状态树 store,只要把他们经过 connect 函数调用。

我不是说要把每个组件都用 connect 函数调用,那是一个很糟糕的作法(设计混乱并且太慢了)。

Provider 看起来很具备魔性,实际上在挂载的时候使用了 React 的 “context” 特性。

Provider 就像一个秘密通道链接到了每个组件,使用 connect 打开了通向每个组件的大门。

想象一下,把糖浆倒在一堆煎饼上,假如你只把糖浆倒在了最上面的煎饼上,怎么才能让全部的煎饼都能蘸到糖浆呢。 Provider 为 Redux 作了这件事。

在文件 src/index.js中,导入 Provider 组件而且用它来包裹 App 组件的内容。

import { Provider } from 'react-redux';
...

const App = () => (
  <Provider>
    <Counter/>
  </Provider>
);
复制代码

咱们仍然会遇到报错,由于 Provider 须要一个 store 状态树才能起做用,它会把 store 做为属性,因此咱们首先须要建立一个 store。

建立一个 store

Redux 使用一个方便的函数来建立 stores,这个函数就是 createStore。好了,如今让咱们来建立一个 store 而后把它做为属性传入 Provider 组件:

import { createStore } from 'redux';

const store = createStore();

const App = () => (
  <Provider store={store}>
    <Counter/>
  </Provider>
);
复制代码

又产生了另一个不一样的错误:

Expected the reducer to be a function.

如今是 Redux 的问题了,Redux 不是那么的智能,你可能但愿建立一个 store,它就会从 store 中 给你一个中很好的默认的值,哪怕是一个空对象?

可是毫不会这样,Redux 不会对你的状态的组成作出任何的猜想,状态的组成结构彻底取决于你本身。他能够是一个对象, 一个数字, 一个字符串, 或者是你须要的任何形式。因此咱们必须提供一个函数去返回这个状态,这个函数就叫作reducer(后面会解释为何这么命名)。让咱们来看看函数最简单的状况,将它做为函数 createStore 的参数,看看会发生什么:

function reducer() {
  // just gonna leave this blank for now
  // which is the same as `return undefined;`
}

const store = createStore(reducer);
复制代码

Reducer 必需要有返回值

又产生了另外的错误:

Cannot read property ‘count’ of undefined

产生这个错误是由于咱们试图去取得 state.count,可是 state 却没有定义。Redux 但愿 reducer 函数为 state 返回一个值,而不是返回一个 undefined

reducer 函数应该返回一个状态,实际上它应该用利用当前状态去返回新的状态

让咱们用 reducer 函数去返回知足咱们须要的状态形式:一个含有 count 属性的对象。

function reducer() {
  return {
    count: 42
  };
}
复制代码

嘿!这个 count 如今显示为 “42”,神奇吧。

只是有一个问题:count 一直显示为42。

目前为止

在咱们进一步了解怎么更新计数器的值以前,咱们先来了解一下到目前为止咱们作了些什么:

  • 咱们写了一个 mapStateToProps 函数,该函数的做用是:把 Redux 中的状态转换成一个包含属性的对象。
  • 咱们用模块 react-redux 中的函数 connect 把 Redux store 状态树和 Counter 组件链接起来,使用 mapStateToProps 函数配置了怎么联系。
  • 咱们建立了一个 reducer 函数去告诉 Redux 咱们的状态应该是什么形式的。
  • 咱们使用 reducercreateStore 函数的参数,用它建立了一个 store。
  • 咱们把整个组件包裹在了 react-redux 中的组件 Provider 中,向该组件传入了 store 做为属性。
  • 这个程序工做的很好,惟一的问题是计数器显示停留在了42。

你跟着我作到如今了吗?

互动起来 (让计数器工做)

我知道到目前为止咱们的程序是不好劲的,大家已经写了一个显示着数字 “42” 和两个无效的按钮的静态的 HTML 页面,不过你还在继续阅读,接下来将继续用 React 和 Redux 和其它的一些东西让咱们的程序变得复杂起来。

我保证接下来作的事情会让上面作的一切都值得。

事实上,我收回刚才那句话,一个简单的计数器的例子是一个很好的教学例子,可是 Redux 让应用变得复杂了,React 的 state 应用起来其实也很简单,甚至通常的 JS 代码也可以实现的很好,挑选正确的工具作正确的事,Redux 不老是那个合适的工具,不过我偏题了。

初始化状态

咱们须要一个方式去告诉 Redux 改变计数器的值。

还记得咱们写的 reducer 函数吗?(固然你确定记得,由于那是两分钟以前的事)。

还记得我说过它会使用当前状态返回新的状态吗?好的,我再重复一次,实际上,它使用当前状态和一个 action 做为参数,而后返回一个新的状态,咱们应该这样写:

function reducer(state, action) {
  return {
    count: 42
  };
}
复制代码

Redux 第一次调用这个函数的时候会以 undefined 做为实参替代 state,意味着返回的是初始状态,对于咱们来讲,可能返回的是一个属性 count 值为 0 的对象。

在 reducer 上面写初始状态是很常见的,当 state 参数未定义的时候,使用 ES6 的默认参数的特性为 state 参数提供一个参数。

const initialState = {
  count: 0
};

function reducer(state = initialState, action) {
  return state;
}
复制代码

这样子试试呢,代码仍然会起做用,不过如今计数器停留在了 0 而不是 42,多么让人惊讶。

Action

咱们最后谈谈 action 参数,这是什么呢?它来自哪里呢? 咱们怎么用它去改变不变的 counter 呢?

一个 “action” 是一个描述了咱们想要改变什么的 JS 对象,为一个要求就是对象必需要有一个 type 属性,它的值应该是一个字符串,这里有一个例子:

{
  type: "INCREMENT"
}
复制代码

这是另一个例子:

{
  type: "DECREMENT"
}
复制代码

你的大脑在快速运转吗?你知道接下来咱们要作什么吗?

对 Actions 作出响应

还记得 reducer 的做用是用当前状态和一个action去计算出新的状态吧。因此若是一个 reducer 接受了一个 action 例如 { type: "INCREMENT" },你想要返回什么做为新的状态呢?

若是你像下面这样想,那么你就想对了:

function reducer(state = initialState, action) {
  if(action.type === "INCREMENT") {
    return {
      count: state.count + 1
    };
  }

  return state;
}
复制代码

使用 switch 语句和 case 语句处理每个 action 是很常见的写法把你的 reducer 函数写成下面这样子:

function reducer(state = initialState, action) {
  switch(action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1
      };
    case 'DECREMENT':
      return {
        count: state.count - 1
      };
    default:
      return state;
  }
}
复制代码

老是返回一个状态

你会注意到函数默认返回的是 return state。这很重要,由于 action 不知道要作什么,Redux 经过 action 去调用你的 reducer 函数。实际上 你接受的第一个 action 是 { type: "@@redux/INIT" }。试着在 switch 前面写一个 console.log(action) 看看会打印出什么。

还记得 reducer 的工做是返回一个新状态吧,即便当前状态没有发生改变也要返回。 你不想从 “有一个状态” 变成 “state = undefined” 吧? 在你忘了 default 状况的时候就会发生这样的事,不要这样作。

永远不要改变状态

永远不要去作这件事:不要改变 state。State 是不可变的。你不能够改变它,意味着你不能这样作:

function brokenReducer(state = initialState, action) {
  switch(action.type) {
    case 'INCREMENT':
      // 不,不要这样作,这样正在改变状态
      state.count++;
      return state;

    case 'DECREMENT':
      // 不要这样作,这也是在改变状态
      state.count--;
      return state;

    default:
      // 这样作是很好的.
      return state;
  }
}
复制代码

你也不要作这样的事,好比写 state.foo = 7 或者 state.items.push(newItem),或者 delete state.something

把这想象为一场游戏,你惟一能作的事就是 return { ... },这是一个有趣的游戏,一开始游戏有些让人抓狂,可是随着你的练习你会以为游戏愈来愈有意思。

我编写了一个简短的指南关于怎么去处理不可变的更新,展现了七种常见的包括对象和数组在内的更新模式。

全部的规则…

老是返回一个状态,不要去改变状态,不要链接到每个组件,吃你本身的西蓝花,不要在外面待着超过 11 点...,真累啊。这就像一个规则工厂,我甚至不知道那是什么。

是的,Redux 可能就像一个霸道的父母。可是都是出于爱。来自函数式编程的爱。

Redux 创建在不变性的基础上,由于改变全局的状态就是一条通向毁灭的道路。

你是否使用一个全局对象去保存整个 app 的状态?一开始运行的很好,很容易,而后状态在没有任何预测的状况下发生了改变,并且几乎不可能去找到改变状态的代码。

Redux 使用一些简单的规则去避免了这样的问题,State 是只读的,actions 是惟一修改状态的方式,改变状态只有一种方式:这个方式就是:action -> reducer -> 新的状态。reducer 必须是一个纯函数,它不能修改它的参数。

有插件能够帮助你去记录每个 action,追溯它们,你能够想象到的一切。从时间上追溯调试是建立 Redux 的动机之一。

Actions 来自哪里呢?

让人迷惑的一部分仍然存在:咱们须要一个方式去让一个 action 进入到咱们的 reducer 中,咱们才能增长或者减小这个计数器。

Action 不是被生成的,它们是被dispatched的,有一个小巧的函数叫作dispatch。

dispatch 函数由 Redux store 的实例提供,也就是说,你不能够仅仅经过 import { dispatch }得到 dispatch 函数。你能够调用 store.dispatch(someAction),可是那不是很方便,由于 store 的实例只在一个文件里面能够被得到。

很幸运,咱们还有 connect 函数。除了注入 mapStateToProps 函数的返回值做为属性之外,connect 函数dispatch 函数做为属性注入了组件,使用这么一点知识,咱们又可让计数器工做起来了。

这里是最后的组件形式,若是你一直跟着写到了这里,那么惟一要改变的实现就是 incrementdecrement:它们如今能够调用 dispatch 属性,经过它分发一个 action。

import React from 'react';
import { connect } from 'react-redux';

class Counter extends React.Component {
  increment = () => {
    this.props.dispatch({ type: 'INCREMENT' });
  }

  decrement = () => {
    this.props.dispatch({ type: 'DECREMENT' });
  }

  render() {
    return (
      <div>
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span>{this.props.count}</span>
          <button onClick={this.increment}>+</button>
        </div>
      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
    count: state.count
  };
}

export default connect(mapStateToProps)(Counter);
复制代码

整个项目的代码(它的两个文件)能够在 Github上面找到。

如今怎样了呢?

利用 Counter 程序做为一个传送带,你能够继续学习会更多的 Redux 知识了。

“什么?! 还有更多?!”

还有不少的地方我没有讲到,我但愿这个介绍是容易理解的 – action constants, action 建立函数, 中间件, thunks 和异步调用, selectors, 等等。 还有不少。这个 Redux docs 文档写的很好,覆盖了我讲到的全部知识和更多的知识。

你已经了解到了基本的思想,但愿你理解了数据怎么 Redux 里面变化 (dispatch(action) -> reducer -> new state -> re-render),reducer 作了什么,action 又作了什么,它们是怎么做用在一块儿的。

我将会发布一个新的课程,课程涵盖到全部的这些东西和更多的知识!这里登陆 去关注.

以按部就班的方式学习 React,查看个人 - 免费查看两个示例章节。

就我而言,即便是免费的介绍也是值得的。 — Isaac


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索