就算我花了不少时间,用一种你不会忘记的方式来解释Redux的原则,口头指示也是有其限制的。

为加深对这些原则的理解,我将会向您展示一个例子。如果你愿意的话,可以称它为你的第一个Redux应用程序。

我的教学方式是引入难度逐步加大的例子。所以,首先,这个例子关注于通过一个简单的纯React应用来使用Redux。

这里的目标是理解如何在一个简单React项目中引入Redux,同时也加深对基础Redux概念的理解。

准备好了吗?

如下是一个简单Hello World React应用。

第 2 章:第一个Redux应用程序 - 图1

不要笑它。

我们将从已知的像React这样的概念,逐步扩展到未知的Redux。

Hello World React应用程序的结构

我们要使用的React应用已经用create-react-app脚手架搭建完毕。因此,应用程序的结构是我们已经熟悉的。

推荐从Github中下载库。

这里有一个index.js入口文件渲染一个<App />组件给DOM。

App组件由某个<HelloWorld />组件组成。

这个<HelloWorld />组件带有一个tech属性(prop),这个prop负责显示给用户的特定技术。

比如,<HelloWorld tech="React" />会生成如下:

第 2 章:第一个Redux应用程序 - 图2

同样,<HelloWorld tech="Redux" />会生成:

第 2 章:第一个Redux应用程序 - 图3

如下是App组件的示例代码:

src/App.js

  1. import React, { Component } from "react";
  2. import HelloWorld from "./HelloWorld";
  3. class App extends Component {
  4. state = {
  5. tech : "React"
  6. }
  7. render() {
  8. return <HelloWorld tech={this.state.tech}/>
  9. }
  10. }
  11. export default App;

好好看看state对象。

state对象中只有一个字段tech,它被作为prop向下传给HelloWorld组件:

  1. <HelloWorld tech={this.state.tech}/>

现在还不需要操心HelloWorld组件的实现。它只是传入了一个tech属性,并应用了一些花哨的CSS。仅此而已。

既然本文主要关注于Redux,所以我会忽略样式细节。

那么,现在挑战就来了。

如何重构我们的App来使用Redux?如何去掉state对象,让它完全被Redux管理?记住,Redux是你的应用程序的状态管理器

我们在下一节开始回答这些问题。

回顾你的Redux知识。还记得官方文档的引文吗?
Redux是一个JavaScript应用程序的状态容器,可以提供可预测的状态管理。

上面句子的一个关键词是状态容器

从技术上讲,你想让Redux管理应用程序的状态,就是这让Redux变为状态容器。

你的React组件的状态依然存在。Redux并没有带走它。

不过,Redux会有效管理应用程序的整体状态。像银行金库一样,Redux用store来负责此事。

对于我们这个的简单<App/>组件,state对象很简单。如下就是:

{ 
    tech: "React"
}

我们需要将这个state对象从<App />组件状态中取出来,并让Redux来管理它。

你应该记得银行金库和Redux Store之间的类比。银行金库存放钱,Redux的store存放应用程序state对象。

那么,重构<App />组件来使用Redux的第一步是什么?

是的,你猜对了。就是从<App />内删除组件状态。

Redux的store会负责管理应用程序的状态。就是说,我们需要从<App />中删除当前state对象。

import React, { Component } from "react";
import HelloWorld from "./HelloWorld";

class App extends Component { 
    // state对象已经被删除了。
    render() {  
        return <HelloWorld tech={this.state.tech}/>
    }
}

export default App;

上面的解决方案是不完整的,不过现在<App/>没有状态。

请从命令行界面(CLI)执行yarn install redux,安装Redux。我们需要redux包来做正确事情。

创建一个Redux Store

如果<App />不管理它的状态,那么我们就必须创建一个Redux Store来管理应用程序的状态。

对于银行金库而言,可能需要雇佣两个机械工程是来建一个安全的存钱设备。而对于我们的应用程序而言,要创建一个可管理的保存状态的设施,我们不需要机械工程师。我们只需要在程序中用一些Redux API即可。

如下是创建Redux Store的代码:

import { createStore } from "redux"; //导入redux库
const store = createStore();  // 一个目前还不完整的解决方案

首先,我们从Redux导入createStore()工厂函数。然后,调用该函数createStore()来创建仓库(store)。

createStore()函数带有几个参数。第一个是reducer。所以,较完整的store创建将表示为:createStore(reducer)

现在,让我解释一下为什么这里有一个reducer

store和reducer的关系

回到银行的比喻。

当你到银行取钱时,你与银行柜员见面。在让银行柜员知道WITHDRAW_MONEY意图/action后,他们不仅仅是只把你要的钱递给你。银行柜员首先要确认你的帐号里面有足够的钱,才能执行你要求的取钱事务。

第 2 章:第一个Redux应用程序 - 图4
银行柜员首先要确保你有你要取的钱。从计算机中,他们能看到一切 - 与银行金库的某类沟通,因为金库存有银行所有的钱。总之,银行柜员与金库总是同步的。伟大的朋友!

第 2 章:第一个Redux应用程序 - 图5

对于Redux STORE(我们的金库)和Redux REDUCER(我们的柜员)来说,也可以这么说。Store和reducer是好朋友,总是同步的。

为什么?

REDUCER总是与STORE对话,就像银行雇员与金库保持同步一样。

这就解释了为什么store的创建需要调用一个Reducer,并且这是强制性的。reducer是传给createStore()的唯一强制性参数。

第 2 章:第一个Redux应用程序 - 图6

在下面的小节中,我们会简单介绍reducer,然后通过传递reducercreateStore()工厂函数,来创建一个store

Reducer

我们很快会进入更多的细节,不过现在我还是会保持简短点。

当你听到reducer这个词时,你脑海里面浮现出什么?Reduce?是的,这就是我所想的。它听起来像reduce。

根据Redux官方文档

Reducer是Redux中最重要的概念。

第 2 章:第一个Redux应用程序 - 图7


有经验的工程师可能会喜欢称之为中间件。

我们的银行柜员是相当重要的人,哈?

那么,这跟与Reducer有什么关系。它有什么作用?

用更技术一点的话来讲,reducer也称为reducing函数。你可能没有注意到,但是你可能已经用了reducer - 如果你熟悉Array.reduce()方法的话。

下面我们快速复习一下。

考虑如下的代码。这是得到一个JavaScript数组中值的和的一种流行方式:

let arr = [1,2,3,4,5]()
let sum = arr.reduce((x,y) => x + y)
console.log(sum)  //15

在后台,传给arr.reduce()的函数称为reducer

在本例中,reducer带有两个值,一个累加器和一个当前值,这里x是累加器,y是当前值。

同样,Redux Reducer只是一个函数。一个带有两个参数的函数。第一个是应用程序的state,另一个是action

但是,传给reducerstateaction来自哪里呢?我过去学习Redux时,也多次问过自己这个问题。

首先,我们再来看看Array.reduce()示例:

let arr = [1,2,3,4,5]
let sum = arr.reduce((x,y) => x + y)
console.log(sum)  //15

Array.reduce方法负责传入所需参数xy到函数参数reducer。所以,参数不是凭空而来的。

对于Redux来说也是如此。Redux reducer也被传入某个方法。猜猜它是什么?

就是这样:

createStore(reducer)

createStore()工厂函数。不久之后你会看到,这个过程中还有一点其它的东西。

Array.reduce()一样,createStore()负责将参数传入reducer。

如果你不害怕技术性的东西,那我们就看看Redux源代码中createStore实现的精简版本:

function createStore(reducer) {    
    var state;    
    var listeners = []()

    function getState() {        
        return state    
    }        

    function subscribe(listener) {        
        listeners.push(listener)        
        return unsubscribe() {            
            var index = listeners.indexOf(listener)            
            listeners.splice(index, 1)        
        }    
    }        

    function dispatch(action) {
        state = reducer(state, action) 
        listeners.forEach(listener => listener())    
    } 

    dispatch({})    

    return { dispatch, subscribe, getState }
}

如果你没有看懂上面的代码,也不要崩溃。我真正想指出的东西是在dispatch()函数中。

请观察reducer是如何带stateaction来调用。

也就是说,创建一个redux store的最小代码是:

import { createStore } from "redux";  
const store = createStore(reducer);   //这行已经被更新为包含了创建的reducer.

回到重构过程

下面我们回到重构Hello World React应用程序来使用Redux。

如果我在上一节的任何地方让你迷失了,请再阅读这一节一次,我相信它能被完全理解。

好了,这里是我们现在的所有代码:

import React, { Component } from "react";
import HelloWorld from "./HelloWorld"; 

import { createStore } from "redux";   
const store = createStore(reducer);   

class App extends Component { 
    render() {   
        return <HelloWorld tech={this.state.tech}/> 
    }
}

export default App;

搞清楚了么?

你可能已经注意到这段代码的一个问题。看第4行:传入createStorereducer函数还不存在呢。所以,现在我们需要写一个。还记得吗?reducer只是一个函数。

创建一个新目录reducers,在其中创建一个index.js文件。基本上,我们的reducer函数会在路径src/reducer/index.js中。

首先在这个文件中输出一个简单函数:

export default () => {
}

记住,reducer带有两个参数,正如前面所建立的。现在,我们会关注第一个参数state。将这个参数放入函数中,我们就有如下代码:

export default (state) => {
}

还不错。

一个reducer总是会返回一些东西。在开始的Array.reduce() reducer示例中,我们返回了累加器和当前值的

对于Redux reducer来说,你总是返回应用程序的新状态

且让我来解释一下。

在你走进银行,并成功取钱之后,银行金库为你保存的钱的当前数目不再是一样的。现在,如果你取了200块,你的账户余额就少了200块。银行柜员和金库再次在你有多少钱上面保持同步。

就像银行柜员一样,这正是reducer的工作原理。

像银行柜员一样,reducer总是返回应用程序的新状态,以防万一有所改变。即使执行了取钱操作,我们也不希望发布相同的银行余额。

我们将在后面介绍如何更改/更新状态的内部机制。就目前而言,盲目信任就够了。

现在,回到手头的问题。

因为我们此时不关心改变/更新状态,所以我们将让新状态与传入的状态保持是一样的。

如下是reducer内这句话的表示:

export default (state) => {     
    return state    
}

如果你到银行,而不执行一个动作,那么你的银行余额应该保持一样,对吧?

因此我们没有执行任何ACTION,甚至还没有将它传入到reducer,所以我们会只return同样的state

第二个createStore的参数

当你摆放银行中的柜员时,如果你问他们你的账户余额,他们会查一下,然后告诉你。

第 2 章:第一个Redux应用程序 - 图8

但是怎么查?

当你第一次在你的银行建一个账户时,你要么存一些钱,要么不存。

第 2 章:第一个Redux应用程序 - 图9
我们称此为你帐户的初始存款。

回到Redux。

同样,当你创建一个redux STORE(我们自己存钱的金库)时,可以选择先存入初始存款。


在Redux术语中,这被称为应用的initialState

在代码中,initialState是传入createStore函数调用的第二个参数。

const store = createStore(reducer, initialState);

在作出任何货币动作之前,如果你询问你的银行账户余额,返回给你的总是开户存款。

之后,每次你执行任何货币动作,这个开户存款也会被更新。

现在,这对Redux也是一样。

作为initialState传入的对象就像是金库的开户存款。这个initialState会总是被返回为应用程序的状态,除非通过执行一个action更新了状态。

现在我们将更新应用程序,传入一个initial state

const initialState = { tech: "React " };
const store = createStore(reducer, initialState);

注意,initialState只是一个对象,在我们开始重构之前,它正是我们在React应用程序中的默认状态。

现在,这是我们此时所有的所有代码,reducer也被导入到App中。

App.js

import React, { Component } from "react";
import HelloWorld from "./HelloWorld";
import reducer from "./reducers";
import { createStore } from "redux";  

const initialState = { tech: "React " };
const store = createStore(reducer, initialState);

class App extends Component { 
    render() {   
        return <HelloWorld tech={this.state.tech}/> 
    } 
}

export default App;

reducers/index.js

export default state  => {      
    return state    
}

如果您正在写代码,并尝试现在运行该应用程序,则会出现错误。为什么呢?

看一看传入<HelloWorld />组件的tech属性。它依然是this.state.tech

此时不再有一个state对象绑定到<App />,所以它会是undefined

下面我们来纠正好了。

解决方案相当简单。既然现在是store管理应用程序的状态,这意味着应用程序的state对象必须从store来获取。但是如何获取呢?

当你用createStore()创建一个store时,被创建的store有三个暴露的方法。

其中之一是getState()

在任何时候,在创建了的store上调用getState方法会返回应用程序的当前状态。

在我们的例子中,store.getState()会返回对象{ tech: "React"},因为这是我们在创建Store时传入createStore()的初始状态。

你现在看到这一切是如何聚在一起了吧?

因此,tech属性会被按照如下方式传入<HelloWorld />

App.js

import React, { Component } from "react";
import HelloWorld from "./HelloWorld";
import { createStore } from "redux";  

const initialState = { tech: "React " };
const store = createStore(reducer, initialState);  

class App extends Component { 
    render() {   
        return <HelloWorld tech={store.getState().tech}/> 
    } 
}

第 2 章:第一个Redux应用程序 - 图10

Reducers/Reducer.js

export default state => {       
    return state    
}

就是这样了!你刚学了Redux的基础知识,并成功将一个简单React应用重构为使用Redux。

React应用程序现在有了它被Redux管理的状态。无论从state对象获得什么,都将从上面所示的store中获取。

希望你理解了整个重构过程。

为快速浏览,请看看这个Github diff

通过Hello World项目,我们仔细研究了一些重要的Redux概念。尽管这是一个很小的项目,但它提供了一个体面的基础!

可能的问题

在刚刚结束的Hello World示例中,您可能想出的从store中获取state的解决方案可能如下所示:

class App extends Component {  
    state = store.getState();  
    render() {    
        return <HelloWorld tech={this.state.tech} />;  
    }
}

你怎么看?这会起作用么?

提醒一下,以下两种方法是初始化React组件的状态的正确方法。

(a)

class App extends Component { 
    constructor(props) {   
        super(props);   
        this.state = {}  
    }
}

(b)

class App extends Component {  
    state = {}
}

所以,回到问题的答案,是的,这个解决方案将工作得很好。

store.getState()会从Redux STORE中获取当前状态。

不过,赋值state = store.getState()会把从Redux获取的状态赋值给<App />组件的状态。

弦外之意,从render的返回语句,比如<HelloWorld tech={this.state.tech} />将是有效的。

请注意,这是this.state.tech,而不是store.getState().tech

即使这能起作用,但是它也违背了Redux的理想哲学。

如果在应用程序内,你现在运行this.setState(),应用程序的状态不需要Redux的帮助也会更新。

这是默认的React机制,而它不是你想要的。你想让state被Redux store所管理,从而成为单一数据源。

不管你是在store.getState()中获取状态,还是更新/修改state(我们稍后会介绍),你希望它完全由Redux管理,而不是由setState()

由于Redux管理应用程序的状态,所以您只需从Redux的STORE中填入state作为所需组件的属性(props)。

你可能会问自己的另一个大问题是:为什么我必须经历所有这些压力只是让我的应用程序的状态由Redux管理?

Reducer、store、createStore等等等等等等 …

耶,我明白了。

我也有这种感觉。

不过,考虑一下这样一个事实,就是你不会仅仅去银行,而不遵循正当程序取回自己的钱。这是你的钱,不过你必须遵循适当的程序。

对于Redux来说也是如此。

Redux有它自己做事情的流程。我们必须学习它的工作原理 - 而且你做得还不错!

总结

本章令人兴奋。我们的重点主要是为更有趣的事情奠定良好的基础。

以下是本章学到的一些内容:

  • Redux是JavaScript应用程序的一个可预测的状态容器。
  • Redux的createStore工厂函数被用于创建一个Redux STORE
  • Reducer是必须传递到createStore()的唯一强制性参数。
  • 一个reducer只是一个函数,一个带有两个参数的函数。第一个是应用程序的STATE,另一个是ACTION
  • Reducer总是返回应用程序的新状态
  • 应用程序的初始状态initialState是传给createStore函数调用的第二个参数。
  • Store.getState()会返回应用程序的当前状态。这里Store是一个有效的Redux STORE

介绍练习

请不要跳过练习。特别是如果你对自己的Redux技能没有信心,并且真的想发挥本指南的最大功效。

所以,抓住你的开发者帽子,写点代码:)

此外,如果您希望我在任何时间点就您的任何解决方案给予您反馈,请使用标签#UnderstandingRedux向我发送推文,我很乐意看一看。我不会承诺每一条推文,但我一定会尝试!

一旦你完成了练习,我会在下一节中看到你。

请记住,阅读长内容的一个好方法是将其分解成更短的易消化的单元。这些练习可以帮助你做到这一点。你抽出一些时间,尝试解决这些练习,然后回过头来阅读。这是一种高效的学习方式。

第 2 章:第一个Redux应用程序 - 图11

想看到我对这些练习的解决方案?我已经在书包中包含了练习的解决方案。一旦下载(免费)电子书(PDF&Epub),您将找到如何获取随附代码和练习解决方案的说明。

那么,如下是本节的习题。

练习

(a) 重构user card应用使用Redux

在本书附带的代码文件中,您会发现仅由React编写的用户卡应用程序。该应用程序的状态通过React进行管理。您的任务是将状态移至仅由Redux管理。

第 2 章:第一个Redux应用程序 - 图12