—— 基础知识、JSX介绍、React 元素、组件和属性、状态和生命周期
此文档来自 React 官方文档,在英文原文的基础上进行了增删改,用于我本人的研究与学习,暂不支持转载。因为本人的水平问题,来取经的同学也请慎用。
React 特点
- 组件化,适用于高交互的大型系统
- 单向数据流,数据的变化易于追踪,缺点就是:实现复杂,需要写各种 action 来应对 UI 的变化。
JSX介绍
先来看一句代码。
const element = <h1>Hello, world!</h1>;
这就是 JSX,是 JavaScript 的一种格式扩展。我们推荐在 React 中使用它来描述 UI。
JSX 很像是一种模板语言,但是它其实都是由 JavaScript 实现。
JSX 会创建 React 元素,在下节中我们将学习其加入 DOM 中。
在后面你可以获得必要的 JSX 基础知识。
在 JSX 中嵌入表达式
你可以在 JSX 中嵌入任意 JavaScript 代码,只要将其放入大括号中。
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);
JSX 也是一种表达式
经过编译之后,JSX 会变为常规的 JavaScript 代码。 所以,你可以将 JSX 放入 if 或者 for 表达式中,将其分配为变量,将其作为参数,或者作为函数的返回值。
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
JSX 中给属性赋值
可以通过双引号来赋值字符串属性。
const element = <div tabIndex="0"></div>;
也可以嵌入 JavaScript 代码来为属性赋值。
const element = <img src={user.avatarUrl}></img>;
两个不要同时使用。
注意:
因为 JSX 与 JavaScript 的关系比 HTML 更近一些,所以 React DOM 使用驼峰法命名。比如 class 使用 className,tabindex 使用 tabIndex。
在 JSX 中声明子节点
如果节点没有子节点,即可用 /> 立刻关闭节点,就像 XML 一样。
const element = <img src={user.avatarUrl} />;
如果 JSX 包含子节点。
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
JSX 可以防范脚本注入攻击
在 JSX 中嵌入用户输入是很安全的。
const title = response.potentiallyMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;
通常情况下,React DOM 会在处理 JSX 之前将所有输入内容转换为字符串从而忽略其中的所有值。因此可以确保无法注入攻击代码。
JSX 相当于 Objects
Babel 通过调用方法 React.createElement() 来编译 JSX。
以下两个例子是相同的。
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
React.createElement() 会提供一些简单的检查来帮助你编写无 bug 的代码。但是本质上通过下面的方法来创建对象。
// Note: this structure is simplified
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world'
}
};
这些对象叫做 React 元素。你可以将它们看作 你想在屏幕上看到的东西的 描述( You can think of them as descriptions of what you want to see on the screen.)。
React 会读取这些对象并把它们添加到 DOM 中并且让其始终保持最新。
Tip:
我们推荐你使用 Babel 给你的语言添加标签,可以让 ES6 和 JSX 代码都可以正确的高亮显示。
元素渲染
元素是React应用中的最小组成单位。
元素描述的是你在屏幕上看到的内容。
const element = <h1>Hello, world</h1>;
不同于 DOM 节点,React 元素更加轻量,并且易于创建。React DOM 会更新 DOM 来匹配 React 元素。
在 DOM 中渲染元素
假设你的 HTML 文档里有一个 <div> 节点。
<div id="root"></div>
我们把这个节点命名为 root,因为在其中的所有东西都被 React DOM 接管。
React 应用通常都有一个 root DOM 节点。如果你是在已存在的应用中添加 React,你可以指定多个 root 节点。
要在 root DOM 节点里渲染 React 元素,需要使用下面方法。
方法:
ReactDOM.render(React Element, HTML DOM);
const element = <h1>Hello, world</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);
它会在页面上展示“Hello, world”。
已渲染元素的更新
React 元素是不可改变的。一旦创造了 React 元素,其元素属性和子元素就不可以改变了。一个元素就像是电影里的一帧,它表现了某个特定时间节点的 UI。
所以更改 UI 的唯一方法就是创造一个新的元素,并将其通过ReactDOM.render() 方法添加到 DOM 中。
来看下面的秒表案例:
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
每过一秒都会在 setInterval 的回调函数中调用 ReactDom.render() 。
React 只会更新发生改变的部分
React 在更新元素时会与之前的元素及其子元素进行比较,只会更新两者的不同之处。
所以在 React 中我们只需要考虑下一时刻我们想要的 UI 效果是什么样的,而不是考虑如何通过目前存在的 UI 效果来改变。
在上面的例子中,即使看起来我们每秒都创建了一个描述整个 UI 树的元素,但是其实每次都只有一个文字节点每次在被 React DOM 所更新。
在我们的经验看来,思考每一刻 UI 该是什么样子 会比 思考每一刻如何对它进行改变 少很多 bug。
组件和属性
组件会将 UI 分成彼此之间互不依赖,且可重用的部分,并分别思考每一部分的实现。
从概念上来看,组件就像是 JavaScript 的 functions。接受任意的输入数据(称为“props”)然后返回要在屏幕上显示的 React 元素。
函数式的和类式的组件
最简单的定义组件的方法是写一个JavaScript 函数。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
这个函数是一个有效的 React 组件,因为它接受一个属性对象作为输入参数,返回一个React 元素。我们把它称作函数式的是因为它确实是一个 JavaScript 函数。
你也可以利用 ES6 来定义一个组件。
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
在React 的角度来看,上面两个组件是完全等价的。
组件渲染
之前我们只遇到过带 HTML 标签的元素。
const element = <div />;
然而元素也可以使用用户自定义标签的组件。
const element = <Welcome name="Sara" />;
当 React 看到用户自定的组件出现在了元素中,就会将组件中 JSX 属性值作为一个对象传递给组件,我们把这个对象叫做 props。
例如:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
);
上述代码的执行步骤如下:
- 我们调用 ReactDOM.render() 来处理元素。
- React 将 {name: 'Sara'} 作为 props 调用 Welcome 组件。
- Welcome 组件 返回一个 <h1>Hello, Sara</h1> 元素作为结果。
- React DOM 会高效地更新 DOM 来匹配 <h1>Hello, Sara</h1>。
注意:
组件 要以大写字母开头。
组件构成
组件可以在返回值中返回其他组件。这就让我们在每层的内容中都使用组件这一共同概念。一个按钮,一个表单,一段对话,整个屏幕都被统称为组件。
例如,我们来创建一个多次使用 Welcome 组件的 App。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
通常,在一个React App 中使用一个 App 组件作为应用的最顶层。然而,如果你是将 React 融入一个已存在的 App 中,则需要自下而上,比如一个按钮开始逐渐到达应用的最顶层。
注意:
组件必须返回单个元素,如果要返回多个元素,必须用单个元素包裹起来。
组件提取
不要害怕将组件分割为多个更小的组件。
来看下面这个例子:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
它接受 author(一个对象),text(一个字符串)和 date(一个 date 类型)作为属性,在一个网站上作为评论区存在。
这个组件里的数据很难进行更新,因为这个组件的结构较复杂,而且重用其中的部件也比较麻烦,所以我们来着手分离一些组件出来。
首先,我们将头像 Avatar 分离出来。
function Avatar(props) {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
Avatar 组件不需要知道它是被用在 Comment 组件之中。这就是为什么我们在给其属性命名时使用了 user 而不是 author。
我们建议在命名组件的属性时站在这个属性的角度命名即可,不必要考虑其所应用的上下文环境。
现在我们对 Comment 组件进行了一点精简:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author} />
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
接下来我们我们继续提取一个 UserInfo 组件,包括 Avatar 组件和用户名:
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}
对 Comment 组件进行了进一步简化:
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
分开多个组件在刚开始看来是一个麻烦的工作,但是在大型应用中可以创造很多可重用的组件。划分组件的一个较好的标准是如果一个 UI 运用很多次(比如按钮,面板,头像)或者一个组件足够复杂,都可以将其作为独立组件。
属性是只读的
无论你声明一个函数或类作为组件,都必须保持其参数不被改变。
看下面这个 sum 函数:
function sum(a, b) {
return a + b;
}
上面的函数叫做纯函数(指不依赖于且不改变它作用域之外的变量状态的函数),因为它没有修改其输入,而且在相同的输入下总是返回相同的值。
作为对比,下面的函数式非纯函数,因为它修改了自己的输入值:
function withdraw(account, amount) {
account.total -= amount;
}
React 是很灵活的,但是它有一个很严格的规则:
所有的 React 组件必须是一个不能修改输入的纯函数。
当然,应用的 UI 是动态的而且时刻在改变。在下一节中,我们会介绍一个新的概念“state(状态)”。状态提供了一种在用户操作,网络请求或其他情况下修改组件输出而不违背上述规则的方法。
状态和生命周期
回想一下前几节的秒表案例。
到目前为止我们只学习了一种更新 UI 的方法,即调用 ReactDOM.render () 来更改渲染的结果。
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
在本节中,我们会学习如何这个封装 Clock 组件,将会创造一个它的专属定时器然后让它每秒更新自己。
首先来看一下秒表应该是什么样的:
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
);
}
setInterval(tick, 1000);
然而,这样忽略了一个很重要的需求:实际上设置一个定时器并且每秒都进行更新应该是属于 Clock 内部的实现细节。
理想情况下,我们只想要写一次,然后让 Clock 进行自我更新。
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
要想实现上述想法,我们需要将 状态 引入 Clock 组件。
状态与属性类似,但状态是私有的,而且被组件完全控制。
之前提到过,用类的方法定义组件会有额外的特性,就是局部状态了——只能通过类定义来获得。
由函数向类转变
你可以用下面五步将一个函数式的组件转换为类。
- 创建一个同名的 ES6 类,作为 React.Component 的扩展。
- 添加一个空方法 render()。
- 将函数的内容放入 render 方法中。
- 将 render 方法中的 props 用 this.props 代替。
- 删除遗留的函数声明。
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
Clock 现在是以类定义的而非函数了。
在这我们就可以使用额外的特性,像局部状态和生命周期方法。
在类中添加局部状态
我们将用下面的步骤来将 date 由 属性 转化为 状态。
- 在 render() 方法中使用 this.state.date 替换 this.props.date:
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
- 添加一个 类的构造函数来分配 this.state:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
注意我们是如何向底层构造函数传递 props 的。
constructor(props) {
super(props);
this.state = {date: new Date()};
}
类组件应该调用底层的构造函数。
- 在 <Clock /> 元素中移除 date 属性:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
稍候我们会在组件中添加定时器相关代码。
现在的结果如下所示:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
接下来,我们会在 Clock 中添加计时器让它每秒更新自己。
在类中添加生命周期方法
在包含很多组件的应用中,当组件销毁时释放其所占用的资源是十分重要的。
我们想在 Clock 第一次渲染时在 DOM 中时设置一个定时器,这在 React 中叫做挂载(Mounting)。
我们也想在 Clock 在 DOM 中移除时清除定时器,这个在 React 中叫做解挂(Unmounting)。
我们可以声明一些特殊的方法在组件挂载和解挂时执行。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
}
componentWillUnmount() {
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
这些方法叫做“生命周期钩子”(很不习惯 钩子 这个词,以后就称方法了)。
componentDidMount() 这个方法会在 组件输出渲染在 DOM 上之后运行。这像是一个放置定时器的好地方:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
注意我们是怎样将 timerID 保存给 this 的。
当 this.props 已经给 React 使用并且 this.state 有特殊意义,你可以手动添加另外的变量如果你需要存储东西而且不将它们进行可视化输出。
如果变量不在 render() 中使用,它就不应该出现在状态里。
我们要在 componentWillUnmount() 里对计时器进行销毁。
componentWillUnmount() {
clearInterval(this.timerID);
}
最后,我们要实现一个 tick () 方法来让 Clock 组件每秒更新。
方法中会使用 this.setState() 来更新组件的局部状态。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
现在秒表可以读秒了。
我们来快速总结一下我们做了什么,调用了哪些方法:
- 当 <Clock /> 被传递给 ReactDOM.render(),React 调用了 Clock 组件的构造函数。因为 Clock 需要使用当前时间,所以使用包含当前事假你的对象对 this.state 进行了初始化。稍后会更新这个状态。
- 接下来 React 调用了 Clock 组件的 render() 方法。从这个方法 React 可以知道应该在屏幕上显示什么东西。React 会根据 Clock render() 的输出来调整 DOM。
- 当 Clock 输出被添加到 DOM 中,React 调用 componentDidMount() 生命周期方法。在这个方法里,Clock 组件要求浏览器设置一个定时器来每秒调用组件里的 tick() 方法。
- 每秒浏览器都会调用 tick() 方法。在这个方法里,Clock 组件通过将一个包含当前时间的对象传入 setState() 方法来安排 UI 的更新。感谢 setState(),React 知道 state 已经改变,然后又调用 render() 方法来知晓屏幕上应该显示什么。这次,render() 方法中的 this.state.date 发生了改变,render 的输出也会包含更新后的时间。因此 React 更新了 DOM。
- 如果 Clock 组件被移出 DOM,React 调用 componentWillUnmount() 生命周期方法来停止定时器。
正确使用 State
关于 setState() 你要明白三件事。
不要直接修改 State
如下的方式不会重新渲染组件:
// Wrong
this.state.comment = 'Hello';
相反的,使用 setState():
// Correct
this.setState({comment: 'Hello'});
唯一可以分配 this.state 的地方是构造函数中。
State 的更新可能是异步的
在单次的更新中 React 可能有很多 setState() 的调用。
因为 this.props 和 this.state 可能是异步更新,所以你不应该根据这两个值来计算之后的 state 值。
例如,下面的代码会无法更新 counter:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
可以使用 setState() 的另一种形式来达成目的。setState() 可以接受一个函数,这个函数会接收之前的 state 值作为第一个参数,接收在此刻更新的 props 值作为第二个参数:
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
State 更新被合并
当你调用 setState(),React 将你提供给当前 state 的对象合并起来。
假如,你的属性值有多个独立的变量:
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
然后你可以分别更新它的各个变量:
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
合并是浅合并,所以执行 this.setState({comment}) 不会影响 this.state.posts,但是会完全替代 this.state.comments。
数据顺流而下
对于一个确定的组件来说,无论是其子组件还是父组件都无法知道它是否含有状态 state,而且也不应该关注它到底是以函数定义还是以类定义的。
这就是为什么 状态 state 总是被称为 局部 或者 被封装。状态 state 对于定义使用它的组件之外的都是透明的。
一个组件可以选择将其状态值作为属性传递给它的子组件。
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
对于用户自定组件来说同样适用。
<FormattedDate date={this.state.date} />
FormattedDate 组件会受到 date 作为它的属性值,而且也不会知道它到底来自 Clock 组件的状态还是属性,亦或是手动传入:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
这通常叫做 自上而下 或者 单向数据流。任何的 属性值 state 都是属于特定的组件,而且它所产生任何数据或 UI 也都只能影响下游的组件。
如果你把一个组件树设想成一个由 属性 props 组成的瀑布,每个组件的 状态 state 都像在任意节点加入的额外水流,随着瀑布一起流下。
为了展示每个组件都是相互独立的,我们可以创建一个 App 组件渲染三个 <Clock> 组件:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
每个 Clock 设置了独立的定时器,独立地更新时间。
在 React 应用中,无论一个组件有无状态,其内部实现细节都可以随时改变。你可以在一个有状态组件中使用一个无状态的组件,反之亦然。
网友评论