用户界面在“反应”中被定义为组件。术语组件被许多其他框架使用。我们还可以使用 HTML5 特性(如自定义元素和 HTML 导入)来编写 web 组件。
组件有很多优点,无论我们是使用 HTML5 在本地使用它们,还是使用像 React 这样的库,我们都会获得以下巨大的好处。
考虑这个用户界面:
代码清单 11:基于超文本标记语言的用户界面
<a href=”http://facebook.com”>
<img
src=”facebook.png” />
</a>
这个 UI 代表什么?如果你说 HTML,你可以在这里快速解析它,然后说,“这是一个可点击的图像。”如果我们要把这个 UI 转换成一个组件,也许ClickableImage
是一个好名字。
代码清单 12:基于组件的用户界面
<ClickableImage />
当事情变得更加复杂时,对 HTML 的解析变得更加困难,因此组件允许我们使用我们熟悉的语言(在本例中是英语)快速了解用户界面代表什么。
这里有一个更大的例子:
代码清单 13:基于组件的用户界面
<TweetBox>
<TextAreaWithLimit limit={140} />
<RemainingCharacters />
<TweetButton />
</TweetBox>
不看实际的 HTML 代码,我们就能确切地知道这个 UI 代表什么。此外,如果我们需要修改剩余字符部分的输出,我们知道要去哪里。
将组件视为功能。这个类比其实很接近事实,尤其是在 React 中。函数接受输入,它们做一些事情(可能在那个输入上),然后给我们一个输出。
在函数式编程中,我们也有纯函数,基本上是针对任何外界状态进行保护的;如果我们给他们同样的输入,我们总是会得到同样的输出。
在“反应”中,组件是根据函数建模的。每个组件都有私有属性,就像该函数的输入一样,我们有一个虚拟的 DOM 输出。如果组件不依赖于其定义之外的任何东西(例如,如果它不使用全局变量),那么我们也将该组件标记为纯的。
所有 React 组件都可以在同一个应用程序和多个应用程序中重用。然而,纯组件有更好的机会被重用而没有任何问题。
例如,让我们实现我们的ClickableImage
组件。
代码清单 14: ClickableImage
渲染函数
var ClickableImage = function(props) {
return (
<a href={props.href}>
<img src={props.src} />
</a>
);
};
ReactDOM.render(
<ClickableImage href="http://google.com" src="http://goo.gl/QlB7wl"
/>,
document.getElementById("react")
);
拥有href
和src
属性的变量是这个组件可重用的原因。
注意我们是如何将组件定义为实际函数的。我们可以通过多种方式创建 React 组件。最简单的方法是使用普通的 JavaScript 函数,该函数接收组件的props
作为参数。
该函数的输出是这个组件所表示的虚拟 HTML 视图。
现在不要担心语法——只关注概念。为了重用这个组件,我们可以做一些类似于用谷歌标志渲染ClickableImage
的事情:
props = { href: "http://google.com",
src: "google.png" }
我们可以简单地用不同的道具重用同一个组件:
props
= { href: "http://bing.com", src: "bing.png" }
src
属性应该用实际图像替换,就像我们在前面例子的渲染函数中所做的那样。
我们创建组件来表示视图。对于 ReactDOM,我们定义的 React 组件将表示 HTML DOM 节点。
最后一个例子中的ClickableImage
组件由两个 HTML 元素组成。我们可以把 HTML 元素想象成浏览器中内置的组件。我们也可以使用自己的定制组件来构建更大的组件。例如,让我们编写一个显示搜索引擎列表的组件。
*代码清单 15: SearchEngines
模型
var SearchEngines = function(props) {
return (
<div className="search-engines">
<ClickableImage href="http://google.com"
src="google.png" />
<ClickableImage href="http://bing.com"
src="bing.png" />
</div>
);
}
例如,如果我们有以下格式的数据:
代码清单 16: SearchEngines
数据
var data = [
{ href: "http://google.com", src: "google.png"
},
{ href: "http://bing.com", src: "bing.png" },
{ href: "http://yahoo.com", src: "yahoo.png" }
];
然后,为了使<SearchEngines data={data} />
工作,我们只需将数据数组从对象列表映射到ClickableImage
组件列表:
代码清单 17: SearchEngines
渲染函数
var SearchEngines = function(props) {
return (
<List>
{props.data.map(engine => <ClickableImage {...engine}
/>)}
</List>
);
};
...
ReactDOM.render(
<SearchEngines data={data} />,
document.getElementById("react")
);
...engine
中的三个点表示将引擎的属性展开为ClickableImage
的平面属性,相当于做:
href={engine.href} src={engine.src}
这个SearchEngines
组件现在可以重用了。它可以与我们给它的任何搜索引擎列表一起工作。我们还使用ClickableImage
组件来合成和SearchEngines
组件。
一切最终都会改变。在反应中,组件使用状态对象管理其更改。除了我们作为道具传递给组件的数据之外,React 组件还可以有一个私有状态,它可以随着时间的推移而改变。
例如,计时器组件可以在其状态中存储其当前计时器值。
代码清单 18:计时器组件
<Timer initialSeconds={42} />
这个计时器将从 42 秒开始倒计时到 0,每秒递减一次状态。在第二个 0,它的私有状态将是 42;在第二个 1,私有状态将是 41;在第二个 42,私有状态将为 0。
使用 React,我们只需让计时器组件显示其私有状态。
代码清单 19:定时器渲染函数
function() {
return (
<div>
{this.state.counter}
</div>
)
}
每秒钟,我们递减计数器状态。
代码清单 20:改变状态(伪代码)
Every
second:
state.counter
= state.counter - 1
if
state.counter reaches 0
stop the
timer
好消息是:React 组件能够识别私有状态的变化。当发生变化时,React 会自动为我们点击刷新按钮,并重新呈现组件的用户界面。这就是 React 得名的地方——它将对状态变化做出反应,并将它们反映在用户界面中。
是时候正式学习如何创建 React 组件了。
让我们定义我们的ClickableImage
组件,它理解两个输入属性,href
和src
。一旦我们准备好这个ClickableImage
组件,我们就可以使用ReactDOM
渲染功能将其安装在浏览器中。
代码清单 21:向 DOM 呈现一个反应组件
ReactDOM.render(
<ClickableImage href="google.com",
src="google.com" />,
document.getElementById("react")
);
|
ReactDOM.render
方法的第一个参数是需要渲染的 React 元素,第二个参数是该元素在浏览器中的渲染位置。在这种情况下,我们用id="react"
将其渲染到 HTML 节点。
定义反应组件有三种主要方式:
- 无状态功能组件
- createClass 反应
- 做出反应。成分
由于组件是根据函数建模的,我们可以使用一个普通的 JavaScript 函数来编写纯组件:
代码清单 22:无状态函数组件
var ClickableImage = function(props) {
return (
<a href={props.href}>
<img src={props.src} />
</a>
);
};
当我们使用功能组件时,我们不是从组件类创建实例;相反,函数本身代表了常规组件定义中的呈现方法。如果我们以功能性和声明性的方式设计我们的应用程序,我们的大多数组件可能只是简单的无状态功能组件。
无状态函数组件不能有任何内部状态,它们不公开任何生命周期方法,我们也不能给它们附加任何引用。如果我们需要这些特性中的任何一个(我将在后面的章节中解释),我们将需要一个基于类的组件定义。
使用新的 ES2015 箭头函数语法,可以更简洁地定义ClickableImage
组件:
代码清单 23:带有箭头函数的无状态函数组件
var ClickableImage = props => (
<a href={props.href}>
<img src={props.src} />
</a>
);
React 有一个官方的 API 来定义一个有状态的组件。我们的简单ClickableImage
组件将是:
代码清单 24: React.createClass
语法
var ClickableImage = React.createClass({
render: function() {
return (
<a href={this.props.href}>
<img src={this.props.src} />
</a>
);
}
});
createClass
函数接受一个参数,一个 JavaScript 配置对象。该对象需要一个属性,即render
属性,这是我们定义描述其用户界面的组件功能的地方。
注意如何使用createClass
,我们不会将props
对象传递给render
调用。相反,从这个组件类创建的元素可以使用render
函数中的this.props
访问它们的属性。
this
关键字引用了我们在 DOM 中安装的组件的实例(使用ReactDOM.render
)。每次我们挂载一个<ClickableImage />
元素,我们都在创建一个ClickableImage
组件类的实例。
在面向对象编程术语中,ClickableImage
是类,<ClickableImage />
是从该类实例化的对象。
借助createClass
,我们可以使用组件对象的私有状态,并且可以在其生命周期方法中调用自定义行为。为了演示这两个概念,让我们实现一个Timer
组件。
首先,我们将如何使用这个Timer
组件:
代码清单 25:呈现计时器组件
ReactDOM.render(
<Timer initialSeconds={42} />,
document.getElementById("react")
);
在考虑私有状态或 tick 操作之前,这个组件的简单定义是:
代码清单 26:定时器组件render()
功能
var Timer = React.createClass({
render: function() {
return (
<div>{this.state.counter}</div>
);
}
});
counter
变量是组件实例中私有状态的一部分。可以使用this.state
访问私有状态对象。
我们的Timer
组件有属性initialSeconds
,这是计数器应该开始的地方。组件实例的属性可以使用this.props
访问,所以如果我们需要读取传递给initialSeconds
属性的值,我们使用this.props.initialSeconds
。
可以使用createClass
定义对象中的getInitialState
函数初始化反应组件的状态。我们从getInitialState
函数返回的任何内容都将被用作组件实例的初始私有状态。
我们希望计数器变量的初始状态以this.props.initialSeconds
开始,因此我们执行以下操作:
代码清单 27:定时器组件初始状态
var Timer = React.createClass({
getInitialState: function() {
return {
counter: this.props.initialSeconds
};
},
render: function() {
return (
<div>{this.state.counter}</div>
);
}
});
现在让我们定义“滴答”操作。我们可以使用一个普通的setInterval
函数来每秒(1000 毫秒)打勾。在区间函数内部,我们需要改变组件状态并减少计数器。
滴答操作应该在组件被渲染到 DOM 之后开始,这样我们就可以确定 DOM 中有一个div
,我们现在可以控制它的内容。为此,我们需要一个生命周期方法。
生命周期方法对我们来说就像钩子一样,在 React 组件实例的生命周期中的特定点定义定制行为。这里我们需要的是componentDidMount()
,它允许我们在组件装入 DOM 后定义一个自定义行为。
代码清单 28:componentDidMount()
中的定时器间隔
var Timer = React.createClass({
getInitialState: function() {
return { counter: this.props.initialSeconds };
},
componentDidMount: function() {
var component = this;
setInterval(function() {
component.setState({
counter: component.state.counter - 1
});
}, 1000);
},
render: function() {
return <div>{this.state.counter}</div>;
}
});
这里有几件事需要注意:
- 我们必须在
setInterval
周围使用一个闭包,这样我们就可以访问其中的this
关键字。这是一个老式的 JavaScript 技巧(新的 ES2015 箭头函数语法不再需要它)。 - 为了改变组件的状态,我们使用了
setState
功能。在 React 中,我们永远不应该将状态直接变异为变量。对状态的所有更改都应使用setState
完成。
这个Timer
组件已经准备好了,只是计时器不会停止,会一直往负侧走。我们可以使用clearTimeout
功能停止定时器。继续尝试为我们的组件做到这一点,然后回来看看下面的完整解决方案。
代码清单 29:定时器组件完整定义
var Timer =
React.createClass({
getInitialState: function() {
return {
counter: this.props.initialSeconds };
},
componentDidMount: function() {
var
component = this, currentCounter;
component.timerId = setInterval(function() {
currentCounter
= component.state.counter;
if
(currentCounter === 1) {
clearInterval(component.timerId);
}
component.setState({ counter: currentCounter - 1 });
}, 1000);
},
render:
function() {
return
<div>{this.state.counter}</div>;
}
});
ReactDOM.render(
<Timer initialSeconds={42}
/>,
document.getElementById("react")
);
ES2015 于 2015 年完成,有了它,我们现在可以使用类语法。类是 JavaScript 的构造函数的语法糖,类可以使用extends
关键字相互继承。
代码清单 30: ES2015 类语法
class Student extends Person { }
有了这条线,我们定义了一个新的Student
类,它继承自一个Person
类。
反应应用编程接口有一个类,我们可以扩展它来定义一个反应组件。我们的ClickableImage
定义变成:
代码清单 31: React.Component
语法
class ClickableImage extends React.Component {
render() {
return (
<a href={this.props.href}>
<img src={this.props.src} />
</a>
);
}
}
在类定义中,render
函数基本相同,只是我们使用了新的 ES2015 语法来定义它。function
这个词在 ES2015 中完全可以避免。
让我们看看我们使用 ES2015 语法的Timer
示例。试着找出不同之处。
代码清单 32:使用类语法的计时器组件
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = { counter: this.props.initialSeconds };
}
componentDidMount() {
let
currentCounter;
this.timerId
= setInterval(() => {
currentCounter
= this.state.counter;
if
(currentCounter === 1) {
clearInterval(this.timerId);
}
this.setState({
counter: currentCounter - 1 });
}, 1000);
}
render() {
return (
<div>{this.state.counter}</div>
);
}
}
以下是解释的差异:
- 我们现在用的不是
getInitialState
而是 ES2015 类的构造函数,只需在构造函数中给this.state
赋值即可。我们需要在那里调用super()
调用来激发React.Component
构造函数调用。 - 在
setInterval
中,我们使用了一个箭头函数,即() => { }
。有了 arrow 函数,我们就不需要像以前那样使用闭包技巧了,因为 arrow 函数有一个默认捕获封闭上下文的词汇绑定“this
”。 - 所有函数都是使用新的函数属性语法定义的。比如
componentDidMount() { ... }
。
组件类、元素和实例
有时你会发现这些术语在指南和教程中混淆了。重要的是要理解我们这里有三种不同的东西:
- 通常被称为“组件”的是类。蓝图。全球定义。在
Timer
例子中,变量Timer
本身就是组件类。 - 另一方面,
<Timer />
是我们从Timer
组件类构造的一个 React 元素。这是一个无状态的、不可变的虚拟 DOM 对象。 - 当一个 React 元素被装载到浏览器的 DOM 中时,它就变成了一个组件实例,这是有状态的。
ReactDOM.render
调用的结果是一个组件实例。*