在上一章中,您了解了 React 组件可以经历三个阶段:
- 安装
- 更新
- 卸载
我们已经讨论了安装和卸载阶段。在本章中,我们将重点介绍更新阶段。在此阶段,React 组件已经插入到 DOM 中。此 DOM 表示组件的当前状态,当该状态更改时,React 需要评估新状态将如何改变先前呈现的 DOM。
React 为我们提供了一些方法,可以影响更新期间将要呈现的内容,以及了解更新发生的时间。这些方法允许我们控制从当前组件状态到下一个组件状态的转换。让我们进一步了解 React 组件更新方法的强大特性。
React 组件有五种生命周期方法,属于组件更新阶段:
componentWillReceiveProps()
shouldComponentUpdate()
componentWillUpdate()
render()
componentDidUpdate()
请参见下图,以便更好地查看:
您已经熟悉了render()
方法。现在我们来讨论其他四种方法。
我们将从StreamTweet
组件中的componentWillReceiveProps()
方法开始。在StreamTweet.js
文件中componentDidMount()
方法后添加以下代码:
componentWillReceiveProps(nextProps) {
console.log('[Snapterest] StreamTweet: 4\. Running componentWillReceiveProps()');
const { tweet: currentTweet } = this.props;
const { tweet: nextTweet } = nextProps;
const currentTweetLength = currentTweet.text.length;
const nextTweetLength = nextTweet.text.length;
const isNumberOfCharactersIncreasing = (nextTweetLength > currentTweetLength);
let headerText;
this.setState({
numberOfCharactersIsIncreasing: isNumberOfCharactersIncreasing
});
if (isNumberOfCharactersIncreasing) {
headerText = 'Number of characters is increasing';
} else {
headerText = 'Latest public photo from Twitter';
}
this.setState({
headerText
});
window.snapterest.numberOfReceivedTweets++;
}
此方法首先在组件生命周期的更新阶段调用。当组件从其父组件接收到新属性时,调用它。
此方法为我们提供了一个使用this.props
对象比较当前组件属性和使用nextProps
对象比较下一个组件属性的机会。基于这种比较,我们可以选择使用this.setState()
函数更新组件的状态,在这种情况下不会触发额外的渲染。
让我们看看实际情况:
const { tweet: currentTweet } = this.props;
const { tweet: nextTweet } = nextProps;
const currentTweetLength = currentTweet.text.length;
const nextTweetLength = nextTweet.text.length;
const isNumberOfCharactersIncreasing = (nextTweetLength > currentTweetLength);
let headerText;
this.setState({
numberOfCharactersIsIncreasing: isNumberOfCharactersIncreasing
});
我们首先得到当前推文和下一条推文的长度。当前通过this.props.tweet
提供,下一个通过nextProps.tweet
提供。然后,我们通过检查下一条 tweet 是否比当前 tweet 长来比较它们的长度。比较结果存储在isNumberOfCharactersIncreasing
变量中。最后,我们通过将numberOfCharactersIsIncreasing
属性设置为isNumberOfCharactersIncreasing
变量的值来更新组件的状态。
然后,我们将标题文本设置如下:
if (isNumberOfCharactersIncreasing) {
headerText = 'Number of characters is increasing';
} else {
headerText = 'Latest public photo from Twitter';
}
this.setState({
headerText
});
如果下一条 tweet 更长,我们将标题文本设置为'Number of characters is increasing'
,否则,我们将其设置为'Latest public photo from Twitter'
。然后,通过将headerText
属性设置为headerText
变量的值,再次更新组件的状态。
注意,我们在componentWillReceiveProps()
方法中调用了this.setState()
函数两次。这是为了说明一点,即无论您在componentWillReceiveProps()
方法中调用this.setState()
多少次,它都不会触发该组件的任何其他渲染。React 进行内部优化,将状态更新批处理在一起。
由于StreamTweet
组件接收到的每一条新 tweet 都会调用一次componentWillReceiveProps()
方法,因此它是计算接收到的 tweet 总数的好地方:
window.snapterest.numberOfReceivedTweets++;
现在我们知道如何检查下一条 tweet 是否比当前显示的 tweet 长,但是我们如何选择不渲染下一条 tweet 呢?
shouldComponentUpdate()
方法允许我们决定下一个组件的状态是否应该触发组件的重新渲染。此方法返回一个布尔值,默认为true
,但可以返回false
,以下组件方法不会被调用:
componentWillUpdate()
render()
componentDidUpdate()
跳过对组件的render()
方法的调用将阻止该组件重新呈现,这反过来将提高应用的性能,因为不会进行额外的 DOM 变化。
此方法在组件生命周期的更新阶段第二次调用。
这个方法对于我们来说是一个很好的地方,可以防止下一条显示一个或更少字符的 tweet。将此代码添加到componentWillReceiveProps()
方法后的StreamTweet
组件:
shouldComponentUpdate(nextProps, nextState) {
console.log('[Snapterest] StreamTweet: 5\. Running shouldComponentUpdate()');
return (nextProps.tweet.text.length > 1);
}
如果下一条 tweet 的长度大于 1,shouldComponentUpdate()
返回true
,并且StreamTweet
组件呈现下一条 tweet。否则返回false
,并且StreamTweet
组件不呈现下一个状态。
在 React 更新 DOM 之前,立即调用componentWillUpdate()
方法*。它获取以下两个参数:*
nextProps
:下一个属性对象nextState
:下一个状态对象
您可以使用这些参数来准备 DOM 更新。但是,您不能在componentWillUpdate()
方法中使用this.setState()
。如果您想更新组件的状态以响应其属性的更改,那么可以在componentWillReceiveProps()
方法中进行更新,当属性更改时,React 将调用该方法。
为了演示何时调用componentWillUpdate()
方法,我们需要将其记录在StreamTweet
组件中。在shouldComponentUpdate()
方法后添加此代码:
componentWillUpdate(nextProps, nextState) {
console.log('[Snapterest] StreamTweet: 6\. Running componentWillUpdate()');
}
在调用方法componentWillUpdate()
后,React 调用执行 DOM 更新的render()
方法。然后,调用componentDidUpdate()
方法。
在 React 更新 DOM 后,立即调用componentDidUpdate()
方法*。它有两个参数:*
prevProps
:前面的属性对象prevState
:前一状态对象
我们将使用此方法与更新的 DOM 交互或执行任何渲染后操作。在我们的StreamTweet
组件中,我们将使用componentDidUpdate()
来增加全局对象中显示的 tweet 数量。在componentWillUpdate()
方法后添加此代码:
componentDidUpdate(prevProps, prevState) {
console.log('[Snapterest] StreamTweet: 7\. Running componentDidUpdate()');
window.snapterest.numberOfDisplayedTweets++;
}
调用componentDidUpdate()
后,更新周期结束。当组件的状态更新或父组件传递新属性时,将启动新循环。或者,当您调用forceUpdate()
方法时,它会触发新的更新周期,但会跳过触发更新的组件上的shouldComponentUpdate()
方法。但是,按照通常的更新阶段,对所有子组件调用shouldComponentUpdate()
。尽量避免使用forceUpdate()
方法;这将提高应用的可维护性。
我们对 React 组件生命周期方法的讨论到此结束。
正如您在上一章中所知道的,我们的StreamTweet
组件呈现了两个子组件:Header
和Tweet
。
让我们创建这些组件。要执行此操作,请导航到~/snapterest/source/components/
并创建Header.js
文件:
import React from 'react';
export const DEFAULT_HEADER_TEXT = 'Default header';
const headerStyle = {
fontSize: '16px',
fontWeight: '300',
display: 'inline-block',
margin: '20px 10px'
};
class Header extends React.Component {
render() {
const { text } = this.props;
return (
<h2 style={headerStyle}>{text}</h2>
);
}
}
Header.defaultProps = {
text: DEFAULT_HEADER_TEXT
};
export default Header;
如您所见,我们的Header
组件是一个呈现h2
元素的无状态组件。标题文本作为this.props.text
属性从父组件传递,这使得该组件灵活,允许我们在需要标题的任何地方重用它。我们将在本书后面再次重用此组件。
注意,h2
元素有一个style
属性。
在 React 中,我们可以在 JavaScript 对象中定义 CSS 规则,然后将该对象作为值传递给 React 元素的style
属性。例如,在此组件中,我们定义引用对象的headerStyle
变量,其中:
- 每个对象键都是 CSS 属性
- 每个对象值都是一个 CSS 值
名称中包含连字符的 CSS 属性应转换为camelCase样式;例如,font-size
变为fontSize
,而font-weight
变为fontWeight
。
在 React 组件中定义 CSS 规则的优点如下:
- 可移植性:您可以在一个 JavaScript 文件中轻松共享组件及其样式
- 封装:使样式内联可以限制样式影响的范围
- 灵活性:可以使用 JavaScript 的强大功能计算 CSS 规则
使用此技术的显著缺点是内容安全策略(CSP)可能会阻止内联样式产生任何影响。您可以在了解更多关于 CSP 的信息 https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP 。
我们的Header
组件有一个属性我们还没有讨论,那就是defaultProps
。如果忘记传递 React 组件所依赖的属性,该怎么办?在这种情况下,组件可以使用defaultProps
属性设置默认属性;考虑下面的例子:
Header.defaultProps = {
text: DEFAULT_HEADER_TEXT
};
在本例中,我们将text
属性的默认值设置为'Default header'
。如果父组件通过了this.props.text
属性,那么它将覆盖默认组件。
接下来,让我们创建Tweet
组件。要执行此操作,请导航到~/snapterest/source/components/
并创建Tweet.js
文件:
import React from 'react';
import PropTypes from 'prop-types';
const tweetStyle = {
position: 'relative',
display: 'inline-block',
width: '300px',
height: '400px',
margin: '10px'
};
const imageStyle = {
maxHeight: '400px',
maxWidth: '100%',
boxShadow: '0px 1px 1px 0px #aaa',
border: '1px solid #fff'
};
class Tweet extends React.Component {
handleImageClick() {
const { tweet, onImageClick } = this.props;
if (onImageClick) {
onImageClick(tweet);
}
}
render() {
const { tweet } = this.props;
const tweetMediaUrl = tweet.media[0].url;
return (
<div style={tweetStyle}>
<img
src={tweetMediaUrl}
onClick={this.handleImageClick}
style={imageStyle}
/>
</div>
);
}
}
Tweet.propTypes = {
tweet: (properties, propertyName, componentName) => {
const tweet = properties[propertyName];
if (! tweet) {
return new Error('Tweet must be set.');
}
if (! tweet.media) {
return new Error('Tweet must have an image.');
}
},
onImageClick: PropTypes.func
};
export default Tweet;
该组件呈现一个带有子元素<img>
的<div>
元素。这两个元素都有内联样式,<img>
元素有一个点击事件处理程序,即this.handleImageClick
:
handleImageClick() {
const { tweet, onImageClick } = this.props;
if (onImageClick) {
onImageClick(tweet);
}
}
当用户点击 tweet 的图像时,Tweet
组件检查父组件是否将this.props.onImageClick
回调函数作为属性传递并调用该函数。this.props.onImageClick
属性是Tweet
组件的可选属性,因此我们需要先检查它是否已传递,然后才能使用它。另一方面,tweet
是必需的属性。
我们如何确保组件接收所有必需的属性?
在 React 中,有一种方法可以使用组件的propTypes
对象验证组件属性:
Component.propTypes = {
propertyName: validator
};
在这个对象中,您需要指定一个属性名和一个验证器函数来确定属性是否有效。React 提供了一些预定义的验证器供您重用。它们都在prop-types
包的PropTypes
对象中提供:
PropTypes.number
:验证属性是否为数字PropTypes.string
:验证属性是否为字符串PropTypes.bool
:这将验证属性是否为布尔值PropTypes.object
:验证属性是否为对象PropTypes.element
:验证属性是否为 React 元素
有关PropTypes
验证器的完整列表,您可以在查看文档 https://facebook.github.io/react/docs/typechecking-with-proptypes.html 。
默认情况下,您使用PropTypes
验证器验证的所有属性都是可选的。您可以用isRequired
链接其中任何一个,以确保在缺少属性时 JavaScript 控制台上显示警告消息:
Component.propTypes = {
propertyName: PropTypes.number.isRequired
};
您还可以指定自己的自定义验证器函数,如果验证失败,该函数将返回一个Error
对象:
Component.propTypes = {
propertyName(properties, propertyName, componentName) {
// ... validation failed
return new Error('A property is not valid.');
}
};
让我们看一下在我们的 Ty1 T1 组件中的对象:
Tweet.propTypes = {
tweet(properties, propertyName, componentName) {
const tweet = properties[propertyName];
if (!tweet) {
return new Error('Tweet must be set.');
}
if (!tweet.media) {
return new Error('Tweet must have an image.');
}
},
onImageClick: PropTypes.func
};
如您所见,我们正在验证两个Tweet
组件属性:tweet
和onImageClick
。
我们使用自定义验证器函数来验证tweet
属性。React 将三个参数传递到此函数:
properties
:这是组件属性对象propertyName
:这是我们正在验证的属性的名称componentName
:组件名称
我们首先检查我们的Tweet
组件是否收到tweet
属性:
const tweet = properties[propertyName];
if (!tweet) {
return new Error('Tweet must be set.');
}
然后,我们假设的tweet
属性是一个对象,并检查该对象是否没有media
属性:
if (!tweet.media) {
return new Error('Tweet must have an image.');
}
这两个检查都返回一个将记录在 JavaScript 控制台中的Error
对象。
我们将验证的另一个Tweet
组件的属性是onImageClick
:
onImageClick: PropTypes.func
我们验证了onImageClick
属性的值是一个函数。在本例中,我们重用了PropTypes
对象提供的验证器函数。如您所见,onImageClick
是一个可选属性,因为我们没有添加isRequired
。
最后,出于性能原因,propTypes
仅在 React 的开发版本中检查。
您可能还记得我们最顶层的层次结构Application
组件有两个子组件:Stream
和Collection
。
到目前为止,我们已经讨论并实现了Stream
组件及其子组件。接下来,我们将关注我们的Collection
组件。
创建~/snapterest/source/components/Collection.js
文件:
import React, { Component } from 'react';
import ReactDOMServer from 'react-dom/server';
import CollectionControls from './CollectionControls';
import TweetList from './TweetList';
import Header from './Header';
class Collection extends Component {
createHtmlMarkupStringOfTweetList = () => {
const { tweets } = this.props;
const htmlString = ReactDOMServer.renderToStaticMarkup(
<TweetList tweets={tweets} />
);
const htmlMarkup = {
html: htmlString
};
return JSON.stringify(htmlMarkup);
}
getListOfTweetIds = () =>
Object.keys(this.props.tweets)
getNumberOfTweetsInCollection = () =>
this.getListOfTweetIds().length
render() {
const numberOfTweetsInCollection = this.getNumberOfTweetsInCollection();
if (numberOfTweetsInCollection > 0) {
const {
tweets,
onRemoveAllTweetsFromCollection,
onRemoveTweetFromCollection
} = this.props;
const htmlMarkup = this.createHtmlMarkupStringOfTweetList();
return (
<div>
<CollectionControls
numberOfTweetsInCollection={numberOfTweetsInCollection}
htmlMarkup={htmlMarkup}
onRemoveAllTweetsFromCollection={onRemoveAllTweetsFromCollection}
/>
<TweetList
tweets={tweets}
onRemoveTweetFromCollection={onRemoveTweetFromCollection}
/>
</div>
);
}
return <Header text="Your collection is empty"/>;
}
}
export default Collection;
我们的Collection
组件负责呈现两件事:
- 用户已收集的推文
- 用于操作该集合的用户界面控件元素
让我们看一下组件的 To.T0A.方法:
render() {
const numberOfTweetsInCollection = this.getNumberOfTweetsInCollection();
if (numberOfTweetsInCollection > 0) {
const {
tweets,
onRemoveAllTweetsFromCollection,
onRemoveTweetFromCollection
} = this.props;
const htmlMarkup = this.createHtmlMarkupStringOfTweetList();
return (
<div>
<CollectionControls
numberOfTweetsInCollection={numberOfTweetsInCollection}
htmlMarkup={htmlMarkup}
onRemoveAllTweetsFromCollection={onRemoveAllTweetsFromCollection}
/>
<TweetList
tweets={tweets}
onRemoveTweetFromCollection={onRemoveTweetFromCollection}
/>
</div>
);
}
return <Header text="Your collection is empty"/>;
}
我们首先使用this.getNumberOfTweetsInCollection()
方法在收集中获得大量推文:
getNumberOfTweetsInCollection = () =>this.getListOfTweetIds().length
此方法反过来,使用另一种方法获取推特 ID 列表:
getListOfTweetIds = () => Object.keys(this.props.tweets);
this.getListOfTweetIds()
函数调用返回一个 tweet id 数组,然后this.getNumberOfTweetsInCollection()
返回该数组的长度。
在我们的render()
方法中,一旦我们知道我们收集的推文数量,我们就必须做出选择:
- 如果集合不是空,则渲染
CollectionControls
和TweetList
组件 - 否则,渲染
Header
组件
所有这些组件都呈现什么?
CollectionControls
组件呈现一个带有集合名称和一组按钮的标题,这些按钮允许用户重命名、清空和导出集合TweetList
组件呈现 tweet 列表Header
组件只是简单地呈现一个带有集合为空的消息的头
这样做的目的是仅在集合不为空时显示集合。在这种情况下,我们将创建四个变量:
const {
tweets,
onRemoveAllTweetsFromCollection,
onRemoveTweetFromCollection
} = this.props;
const htmlMarkup = this.createHtmlMarkupStringOfTweetList();
tweets
变量引用从父组件传递的tweets
属性htmlMarkup
变量引用组件的this.createHtmlMarkupStringOfTweetList()
函数调用返回的字符串onRemoveAllTweetsFromCollection
和onRemoveTweetFromCollection
变量引用从父组件传递的函数
顾名思义,this.createHtmlMarkupStringOfTweetList()
方法创建一个字符串,表示通过呈现TweetList
组件创建的 HTML 标记:
createHtmlMarkupStringOfTweetList = () => {
const { tweets } = this.props;
const htmlString = ReactDOMServer.renderToStaticMarkup(
<TweetList tweets={tweets}/>
);
const htmlMarkup = {
html: htmlString
};
return JSON.stringify(htmlMarkup);
}
createHtmlMarkupStringOfTweetList()
方法使用第 3 章中讨论的ReactDOMServer.renderToStaticMarkup()
函数创建第一个反应元素。我们将TweetList
组件作为其参数传递:
const htmlString = ReactDOMServer.renderToStaticMarkup(
<TweetList tweets={tweets} />
);
此TweetList
组件具有一个tweets
属性,该属性引用父组件传递的tweets
属性。
ReactDOMServer.renderToStaticMarkup()
函数生成的 HTML 字符串存储在htmlString
变量中。然后,我们用引用我们的htmlString
变量的html
属性创建一个新的htmlMarkup
对象。最后,我们使用JSON.stringify()
函数将htmlMarkup
JavaScript 对象转换为 JSON 字符串。JSON.stringify(htmlMarkup)
函数调用的结果就是我们的createHtmlMarkupStringOfTweetList()
方法返回的结果。
该方法演示了 React 组件的灵活性;您可以使用相同的 React 组件来呈现 DOM 元素,并生成可以传递给第三方 API 的 HTML 标记字符串。
另一个有趣的观察是在render()
方法之外使用 JSX 语法。事实上,您可以在源文件的任何地方使用 JSX,甚至在组件类声明之外。
让我们仔细看看当我们的集合是 Ty1 T1 而不是 Ty2 T2 空时,Tyt T0 分量返回什么:
return (
<div>
<CollectionControls
numberOfTweetsInCollection={numberOfTweetsInCollection}
htmlMarkup={htmlMarkup}
onRemoveAllTweetsFromCollection={onRemoveAllTweetsFromCollection}
/>
<TweetList
tweets={tweets}
onRemoveTweetFromCollection={onRemoveTweetFromCollection}
/>
</div>
);
我们将CollectionControls
和TweetList
组件包装在<div>
元素中,因为 React 只允许一个根元素。让我们看看每个组件并讨论它的属性。
我们将以下三个属性传递给CollectionControls
组件:
numberOfTweetsInCollection
属性引用了我们集合中当前的推文数量。htmlMarkup
属性引用我们在此组件中使用createHtmlMarkupStringOfTweetList()
方法生成的 HTML 标记字符串。onRemoveAllTweetsFromCollection
属性引用一个函数,该函数从我们的集合中删除所有 tweet。此功能在Application
组件中实现,并在第 5 章中讨论,使您的 React 组件反应。
我们将这两个属性传递给TweetList
组件:
tweets
属性引用来自父Application
组件的推文。onRemoveTweetFromCollection
属性引用一个函数,该函数从我们存储在Application
组件状态的 tweet 集合中删除 tweet。我们已经在第 5 章中讨论了此功能,使您的反应组分反应。
这就是我们的组成部分。
在本章中,您了解了组件生命周期的更新方法。我们还讨论了如何验证组件属性和设置默认属性值。我们的 Snapterest 应用也取得了良好的进展;我们创建并讨论了Header
、Tweet
和Collection
组件。
在下一章中,我们将重点构建更复杂的 React 组件,并完成 Snapterest 应用的构建!