第 7 章更新您的 Reac t组件

第 7 章更新您的 Reac t组件

Flying
2018-10-15 / 0 评论 / 275 阅读 / 正在检测是否收录...

在上一章中,你了解到 React 组件可以经历三个阶段:

  • 挂载
  • 正在更新
  • 卸载

我们已经讨论了挂载和卸载阶段。在本章中,我们将关注更新阶段。在此阶段,React 组件已经插入到 DOM 中。该 DOM 表示组件的当前状态,当该状态发生变化时,React 需要评估新状态将如何改变先前渲染的 DOM。

React 为我们提供了一些方法来影响更新过程中要渲染的内容,以及了解更新发生的时间。这些方法允许我们控制从当前组件状态到下一个组件状态的转换。让我们进一步了解 React 组件更新方法的强大特性。

了解组件生命周期更新方法

React 组件有五种属于组件更新阶段的生命周期方法:

  • componentWillReceiveProps()
  • shouldComponentUpdate()
  • componentWillUpdate()
  • render()
  • componentDidUpdate()

为更好的理解请参看下图:

lifecycle-updates.jpg

你已经熟悉 render() 方法。现在让我们讨论其他四种方法。

componentWillReceiveProps 方法

我们将从 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 获得,下一条推文可通过 nextProps.tweet 获得。

然后,我们通过检查下一条推文是否比当前推文长来比较它们的长度。比较结果存储在 isNumberOfCharactersIncreaing 变量中。最后我们通过将 numberOfCharactersIsIncreaing 属性设置为 isNumberOfCharactersIncreasing 变量值来更新组件的状态。

然后,我们将标题文本设置如下:

if (isNumberOfCharactersIncreasing) {
  headerText = 'Number of characters is increasing';
} else {
  headerText = 'Latest public photo from Twitter';
}
this.setState({
  headerText,
});

如果下一条推文更长,我们将标题文本设置为 'Number of characters is increasing',否则,我们将其设置为 Latest public photo from Twitter。然后通过将 headerText 属性设置为 headerText 变量的值,再次更新组件的状态。

我们在 componentWillReceiveProps() 方法中两次调用 this.setState() 方法。这是为了说明这一点:无论你在 componentWillReceiveProps() 方法调用多少次 setState() ,都不会触发该组件的任何其他渲染。React 进行内部优化,将状态更新一起批处理。

由于组件 WillReceiveProps() 方法将为 StreamTweet 组件将接收的每个新推文调用一次,因此它是计算接收推文总数的好地方:

window.snapterest.numberOfReceivedTweets++;

现在我们知道如何检查下一条推文是否比当前显示的推文长,但我们如何能选择根本不用渲染下一条推文呢?

shouldComponentUpdate 方法

shouldComponentUpdate() 方法允许我们决定组件的下一个状态是否应该触发组件的重新渲染。该方法返回布尔值,默认值为 true,返回 false 时不会调用以下组件方法:

  • componentWillUpdate()
  • render()
  • componentDidUpdate()

跳过对组件 render() 方法的调用将阻止该组件重新渲染,这相反的会提高应用的性能,因为不会进行额外的 DOM 更新。

该方法在组件生命周期的更新阶段第二个调用。

对于我们来说,这个方法是一个可以防止显示包含一个或更少字符的下一条推文的很好地方。在 componentWillReceiveProps() 方法之后将此代码添加到 StreamTweet 组件:

shouldComponentUpdate(nextProps, nextState) {
  console.log('[Snapterest] StreamTweet: 5. Running
  shouldComponentUpdate()');
  return (nextProps.tweet.text.length > 1);
}

如果下一条推文的长度大于 1,那么 shouldComponentUpdate() 返回 trueStreamTweet 组件将渲染下一条 tweet。否则,它返回 falseStreamTweet 组件不会渲染下一个状态。

componentWillUpdate 方法

在 React 更新 DOM 之前立即调用 componentWillUpdate() 方法。它获得以下两个参数:

  • nextProps:下一个属性对象
  • nextState:下一个状态对象

你可以使用这些参数为 DOM 更新做准备。但是,不能在 componentWillUpdate() 方法中使用 this.setState() 。如果你希望更新组件的状态以响应其属性的更改,那么可以在 componentWillReceiveProps() 方法中进行更新,当属性更改时,React 将调用该方法。

为了演示何时调用 componentWillUpdate() 方法,我们需要将其记录在 StreamTweet 组件中。在 shouldComponentUpdate() 方法之后添加以下代码:

componentWillUpdate(nextProps, nextState){
  console.log('[Snapterest]StreamTweet:6.运行componentWillUpdate');
}

在调用 componentWillUpdate() 方法之后,React 调用执行 DOM 更新的 render() 方法。然后调用 componentDidUpdate() 方法。

componentDidUpdate 方法

在 React 更新 DOM 后立即调用 componentDidUpdate() 方法。它有两个参数:

  • prevProps:上一个属性对象
  • prevState:上一状态对象

我们将使用该方法与更新的 DOM 交互或执行任何渲染后操作。在 StreamTweet 组件中,我们将使用 componentDidUpdate() 方法来增加全局对象中显示的 tweet 的数量。在 componentWillUpdate() 方法之后添加以下代码:

componentDidUpdate(prevProps,prevState){
  console.log('[Snapterest]StreamTweet:7.运行componentDidUpdate() ');
  window.snapterest.numberOfDisplayedTweets++;
}

调用 componentDidUpdate() 后,更新周期结束。当更新组件的状态或父组件传递新属性时,将开始新的循环。或者当你调用 forceUpdate() 方法时,它会触发新的更新周期,但会跳过组件上触发更新的 shouldComponentUpdate() 方法。然而,按照通常的更新阶段在所有子组件上调用 shouldComponentUpdate() ,尽量避免使用 forceUpdate() 方法;这将提高应用的可维护性。

我们对 React 组件生命周期方法的讨论到此结束。

设置 React 组件默认属性

正如你在上一章中所知,StreamTweet 组件渲染两个子组件:HeaderTweet

让我们新建这些组件。转到 ~/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 属性应转换为 驼峰 样式;例如,字体大小变为 fontSize,字体权重变为 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,
};

在本例中,我们为文本属性设置了默认值 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);
  }
}

当用户单击推文的图像时,推文组件检查父组件是否已将 this.props.onImageClick 回调函数作为属性传递并调用该函数。this.props.onImageClick 属性是 Tweet 组件的可选属性,因此我们需要在使用它之前检查它是否被传递。另一方面,Tweet 是必需的属性。

我们如何确保组件接收所有必需的属性?

验证 React 组件属性

React 可以使用组件的 propTypes 对象来验证 React 组件属性:

Component.propTypes = {
  propertyName: validator,
};

在这个对象中,你需要指定一个属性名和一个验证函数,以确定属性是否有效。React 提供了一些预定义的验证器供你重用。它们在 prop-types 包的 PropTypes 对象中都可用:

  • PropTypes.number:这将验证属性是否为数字
  • `PropTypes.string:这将验证属性是否为字符串
  • PropTypes.bool:这将验证属性是否为布尔值
  • PropTypes.object:这将验证属性是否为对象
  • PropTypes.element::这将验证属性是否为 React 元素

有关 PropTypes 验证器的完整列表,你可以查看文档

默认情况下,使用 PropTypes 验证器验证的所有属性都是可选的。你可以使用 isRequired 链接它们中的任何一个,以确保在缺少属性时在 JavaScript 控制台上显示警告消息:

Component.propTypes = {
  propertyName: PropTypes.number.isRequired,
};

你还可以指定自己的自定义验证器函数,如果验证失败,该函数将返回一个 Error 对象:

Component.propTypes = {
  propertyName(properties, propertyName, componentName) {
    // ... 验证失败
    return new Error('A property is not valid.');
  },
};

让我们看看 Tweet 组件中的 propTypes 对象:

static 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 组件属性:TweetonImageClick

我们使用自定义验证器函数来验证 tweet 属性。React 向该函数传递三个参数:

  • properties:这是组件属性对象
  • propertyName:这是我们要验证的属性的名称
  • componentName:这是组件的名称

首先我们检查我们的 Tweet 组件是否收到了 Tweet 属性:

const tweet = properties[propertyName];
if (!tweet) {
  return new Error('Tweet must be set.');
}

然后我们假设 Tweet 属性是一个对象,并检查该对象是否没有媒体属性:

if (!tweet.media) {
  return new Error('Tweet must have an image.');
}

这两个检查都返回一个 Error 对象,该对象将记录在 JavaScript 控制台中。

我们将验证 Tweet 组件的另一个属性 onImageClick

onImageClick: PropTypes.func;

我们验证 onImageClick 属性的值是一个函数。在本例中,我们重用 PropTypes 对象提供的验证器函数。正如你所看到的,onImageClick 是一个可选属性,因为我们没有添加 isRequired

最后出于性能原因,propTypes 只在 React 的开发版本中检查。

新建 Collection 组件

你可能还记得,我们最顶层的层次结构 Application 组件有两个子组件:StreamCollection

到目前为止,我们已经讨论并实现了 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 htmlString = ReactDOMServer.renderToStaticMarkup(
      <TweetList tweets={this.props.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 htmlMarkup = this.createHtmlMarkupStringOfTweetList();
      const tweets = this.props.tweets;
      const removeAllTweetsFromCollection =
        this.props.onRemoveAllTweetsFromCollection;
      const handleRemoveTweetFromCollection =
        this.props.onRemoveTweetFromCollection;

      return (
        <div>
          <CollectionControls
            numberOfTweetsInCollection={numberOfTweetsInCollection}
            htmlMarkup={htmlMarkup}
            onRemoveAllTweetsFromCollection={removeAllTweetsFromCollection}
          />
          <TweetList
            tweets={tweets}
            onRemoveTweetFromCollection={handleRemoveTweetFromCollection}
          />
        </div>
      );
    }

    return <Header text="Your collection is empty" />;
  }
}

export default Collection;

我们的 Collection 组件负责渲染两件事:

  • 用户收集的推文
  • 用于操作集合的用户界面控制元素

让我们来看看组件的 render() 方法:

render() {
  const numberOfTweetsInCollection =
  this.getNumberOfTweetsInCollection();

  if (numberOfTweetsInCollection > 0) {
    const htmlMarkup = this.createHtmlMarkupStringOfTweetList();
    const tweets = this.props.tweets;
    const removeAllTweetsFromCollection =
      this.props.onRemoveAllTweetsFromCollection;
    const handleRemoveTweetFromCollection =
      this.props.onRemoveTweetFromCollection;

    return (
      <div>
        <CollectionControls
          numberOfTweetsInCollection={numberOfTweetsInCollection}
          htmlMarkup={htmlMarkup}
          onRemoveAllTweetsFromCollection=
          {removeAllTweetsFromCollection}
        />
        <TweetList
          tweets={tweets}
          onRemoveTweetFromCollection=
          {handleRemoveTweetFromCollection}
        />
      </div>
    );
  }

  return <Header text="Your collection is empty" />;
}

首先我们使用 this.getNumberOfTweetsInCollection() 方法获取集合中的推文数量:

getNumberOfTweetsInCollection = () => this.getListOfTweetIds().length;

相反的,该方法使用另一种方法获取推文 ID 列表:

getListOfTweetIds = () => Object.keys(this.props.tweets);

getListOfTweetId() 函数调用返回一个推文 ID 数组,然后 getNumberOfTweetsInCollection() 返回该数组的长度。

render() 方法中,一旦我们知道集合中的 tweet 数量,我们就必须做出选择:

  • 如果集合不为空,则渲染 CollectionControlsTweetList 组件
  • 否则渲染 Header 组件

所有这些组件渲染什么呢?

  • CollectionControls 组件渲染具有集合名称和一组按钮的标头,这些按钮允许用户重命名、清空和导出集合
  • TweetList` 组件渲染推文列表
  • Header 组件简单地渲染一个标头,其中包含集合为空的消息

其目的是仅在集合不为空时显示集合。在这种情况下,我们新建了四个变量:

const htmlMarkup = this.createHtmlMarkupStringOfTweetList();
const tweets = this.props.tweets;
const removeAllTweetsFromCollection =
  this.props.onRemoveAllTweetsFromCollection;
const handleRemoveTweetFromCollection = this.props.onRemoveTweetFromCollection;
  • htmlMarkup 变量引用调用组件的 this.createHtmlMarkupStringOfTweetList() 函数返回的字符串。
  • tweets 变量引用从父组件传递的 tweets 属性
  • onRemoveAllTweetsFromCollection 和 onRemoveTweetFromCollection` 变量引用从父组件传递的函数

顾名思义,createHtmlMarkupStringOfTweetList() 方法新建一个字符串,该字符串表示通过渲染 TweetList 组件新建的 HTML 标签:

createHtmlMarkupStringOfTweetList() {
  const htmlString = ReactDOMServer.renderToStaticMarkup(
    <TweetList tweets={this.props.tweets}/>
  );

  const htmlMarkup = {
    html: htmlString
  };

  return JSON.stringify(htmlMarkup);
}

createHtmlMarkupStringOfTweetList() 函数调用 ReactDOMServer.renderToStaticMarkup() 方法,我们曾在第 3 章创建你的第一个 React 元素中讨论过该方法。我们传递 TweetList 组件作为其参数:

const htmlString=ReactDOMServer.renderToStaticMarkup(
  <TweetList tweets={tweets}/>
);

TweetList 组件有一个通过父组件传递的 tweets 属性的引用。

ReactDOMServer.renderToStaticMarkup() 方法生成的 HTML 字符串存储在 htmlString 变量中。然后我们使用引用 htmlString 变量的 html 属性新建一个新的 htmlMarkup 对象。最后我们使用 JSON.stringify() 函数将 htmlMarkup JavaScript 对象转换为 JSON 字符串。我们的 createHtmlMarkupStringOfTweetList()方法返回的是调用 JSON.stringify(htmlMarkup)函数的结果。

该方法展示了 React 组件的灵活性;你可以使用相同的 React 组件来渲染 DOM 元素,并生成可以传递给第三方 API 的 HTML 标签字符串。

另一个有意思的地方是可以在 render() 方法之外使用 JSX 语法。事实上,你可以在源文件中的任何位置使用 JSX,甚至在组件类声明之外。

让我们仔细看看当集合不为空时,Collection 组件返回的内容:

return (
  <div>
    <CollectionControls
      numberOfTweetsInCollection={numberOfTweetsInCollection}
      htmlMarkup={htmlMarkup}
      onRemoveAllTweetsFromCollection={removeAllTweetsFromCollection}
    />
    <TweetList
      tweets={tweets}
      onRemoveTweetFromCollection={handleRemoveTweetFromCollection}
    />
  </div>
);

因为 React 只允许一个根元素,我们将 CollectionControls 组件和 TweetList 组件包装在<div> 元素中,让我们看看每个组件并讨论其属性。

我们将以下三个属性传递给 CollectionControls 组件:

  • numberOfTweetsInCollection 属性引用了我们当前集合中的推文数量。
  • htmlMarkup 属性引用了我们使用 createHtmlMarkupStringOfTweetList() 方法在该组件中生成的 HTML 标签字符串。
  • onRemoveAllTweetsFromCollection 属性引用了一个从集合中删除所有推文的函数。该功能已在 Application 组件中实现,并在第 5 章使你的 React 组件响应中讨论过。

我们将以下两个属性传递给 TweetList 组件:

  • tweets 属性引用从父 Application 组件传递的 tweets 集合
  • onRemoveTweetFromCollection 属性引用了一个函数,该函数从我们存储在 Application 组件状态中的推文集合中删除推文。我们已经在第 5 章让你的 React 组件具有响应式中讨论过这个功能。

这就是我们的 Collection 组件。

总结

在本章中,你了解了组件生命周期的更新方法。我们还讨论了如何验证组件属性和设置属性默认值。我们的 Snapterest 应用也取得了不错的进展;我们创建并讨论了 HeaderTweetCollection 组件。

下一章,我们将专注于构建更复杂的 React 组件,并完成 Snapterest 应用的构建!

10

评论 (0)

取消