就算我花了不少时间,用一种你不会忘记的方式来解释Redux的原则,口头指示也是有其限制的。
为加深对这些原则的理解,我将会向您展示一个例子。如果你愿意的话,可以称它为你的第一个Redux应用程序。
我的教学方式是引入难度逐步加大的例子。所以,首先,这个例子关注于通过一个简单的纯React应用来使用Redux。
这里的目标是理解如何在一个简单React项目中引入Redux,同时也加深对基础Redux概念的理解。
准备好了吗?
如下是一个简单Hello World
React应用。
不要笑它。
我们将从已知的像React这样的概念,逐步扩展到未知的Redux。
Hello World React应用程序的结构
我们要使用的React应用已经用create-react-app
脚手架搭建完毕。因此,应用程序的结构是我们已经熟悉的。
推荐从Github中下载库。
这里有一个index.js
入口文件渲染一个<App />
组件给DOM。
主App
组件由某个<HelloWorld />
组件组成。
这个<HelloWorld />
组件带有一个tech
属性(prop),这个prop负责显示给用户的特定技术。
比如,<HelloWorld tech="React" />
会生成如下:
同样,<HelloWorld tech="Redux" />
会生成:
如下是App
组件的示例代码:
src/App.js
import React, { Component } from "react";
import HelloWorld from "./HelloWorld";
class App extends Component {
state = {
tech : "React"
}
render() {
return <HelloWorld tech={this.state.tech}/>
}
}
export default App;
好好看看state
对象。
在state
对象中只有一个字段tech
,它被作为prop
向下传给HelloWorld
组件:
<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后,他们不仅仅是只把你要的钱递给你。银行柜员首先要确认你的帐号里面有足够的钱,才能执行你要求的取钱事务。
银行柜员首先要确保你有你要取的钱。从计算机中,他们能看到一切 - 与银行金库的某类沟通,因为金库存有银行所有的钱。总之,银行柜员与金库总是同步的。伟大的朋友!
对于Redux STORE
(我们的金库)和Redux REDUCER
(我们的柜员)来说,也可以这么说。Store和reducer是好朋友,总是同步的。
为什么?
REDUCER
总是与STORE
对话,就像银行雇员与金库保持同步一样。
这就解释了为什么store的创建需要调用一个Reducer
,并且这是强制性的。reducer
是传给createStore()
的唯一强制性参数。
在下面的小节中,我们会简单介绍reducer,然后通过传递reducer
给createStore()
工厂函数,来创建一个store
。
Reducer
我们很快会进入更多的细节,不过现在我还是会保持简短点。
当你听到reducer这个词时,你脑海里面浮现出什么?Reduce?是的,这就是我所想的。它听起来像reduce。
根据Redux官方文档:
Reducer是Redux中最重要的概念。
有经验的工程师可能会喜欢称之为中间件。
我们的银行柜员是相当重要的人,哈?
那么,这跟与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
。
但是,传给reducer
的state
和action
来自哪里呢?我过去学习Redux时,也多次问过自己这个问题。
首先,我们再来看看Array.reduce()
示例:
let arr = [1,2,3,4,5]
let sum = arr.reduce((x,y) => x + y)
console.log(sum) //15
Array.reduce
方法负责传入所需参数x
和y
到函数参数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
是如何带state
和action
来调用。
也就是说,创建一个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行:传入createStore
的reducer
函数还不存在呢。所以,现在我们需要写一个。还记得吗?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
的参数
当你摆放银行中的柜员时,如果你问他们你的账户余额,他们会查一下,然后告诉你。
但是怎么查?
当你第一次在你的银行建一个账户时,你要么存一些钱,要么不存。
我们称此为你帐户的初始存款。
回到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}/>
}
}
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
工厂函数被用于创建一个ReduxSTORE
。 Reducer
是必须传递到createStore()
的唯一强制性参数。- 一个
reducer
只是一个函数,一个带有两个参数的函数。第一个是应用程序的STATE
,另一个是ACTION
。 Reducer
总是返回应用程序的新状态
。- 应用程序的初始状态
initialState
是传给createStore
函数调用的第二个参数。 Store.getState()
会返回应用程序的当前状态。这里Store
是一个有效的ReduxSTORE
。
介绍练习
请不要跳过练习。特别是如果你对自己的Redux技能没有信心,并且真的想发挥本指南的最大功效。
所以,抓住你的开发者帽子,写点代码:)
此外,如果您希望我在任何时间点就您的任何解决方案给予您反馈,请使用标签#UnderstandingRedux
向我发送推文,我很乐意看一看。我不会承诺每一条推文,但我一定会尝试!
一旦你完成了练习,我会在下一节中看到你。
请记住,阅读长内容的一个好方法是将其分解成更短的易消化的单元。这些练习可以帮助你做到这一点。你抽出一些时间,尝试解决这些练习,然后回过头来阅读。这是一种高效的学习方式。
想看到我对这些练习的解决方案?我已经在书包中包含了练习的解决方案。一旦下载(免费)电子书(PDF&Epub),您将找到如何获取随附代码和练习解决方案的说明。
那么,如下是本节的习题。
练习
(a) 重构user card应用使用Redux
在本书附带的代码文件中,您会发现仅由React编写的用户卡应用程序。该应用程序的状态通过React进行管理。您的任务是将状态移至仅由Redux管理。