展示型组件和容器型组件

万事开头难。React 也不例外,作为初学者,我们也有一大堆问题。我应该将数据放在何处?如何进行变化通知?如何管理状态?这些问题的答案往往与上下文有关,而有时取决于 React 的实战经验。但是,有一种广泛使用的模式,有助于组织基于 React 的应用,那便是将组件分为展示型组件和容器型组件。

我们先从一个简单示例开始,首先说明此示例的问题,然后将组件分成容器型组件和展示型组件。示例中使用的是 Clock 组件,它接收 Date 对象作为属性并显示实时时间。

  1. class Clock extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = { time: this.props.time };
  5. this._update = this._updateTime.bind(this);
  6. }
  7. render() {
  8. const time = this._formatTime(this.state.time);
  9. return (
  10. <h1>
  11. { time.hours } : { time.minutes } : { time.seconds }
  12. </h1>
  13. );
  14. }
  15. componentDidMount() {
  16. this._interval = setInterval(this._update, 1000);
  17. }
  18. componentWillUnmount() {
  19. clearInterval(this._interval);
  20. }
  21. _formatTime(time) {
  22. var [ hours, minutes, seconds ] = [
  23. time.getHours(),
  24. time.getMinutes(),
  25. time.getSeconds()
  26. ].map(num => num < 10 ? '0' + num : num);
  27. return { hours, minutes, seconds };
  28. }
  29. _updateTime() {
  30. this.setState({
  31. time: new Date(this.state.time.getTime() + 1000)
  32. });
  33. }
  34. };
  35. ReactDOM.render(<Clock time={ new Date() }/>, ...);

在组件的构造函数中,我们初始化了组件的状态,这里只保存了当前时间。通过使用 setInterval ,我们每秒更新一次状态,然后组件会重新渲染。要想看起来像个真正的时钟,我们还使用了两个辅助函数: _formatTime_updateTime_formatTime 用来提取时分秒并确保它们是两位数的形式。_updateTime 用来将 time 对象设置为当前时间加一秒。

问题

这个组件中做了好几件事,它似乎承担了太多的职责。

  • 它通过自身来修改状态。在组件中更改时间可能不是一个好主意,因为只有 Clock 组件知道当前时间。如果系统中的其他部分也需要此数据,那么将很难进行共享。
  • _formatTime 实际上做了两件事,它从时间对象中提取出所需信息,并确保这些值永远以两位数字的形式进行展示。这没什么问题,但如果提取操作不是函数的一部分那就更好了,因为函数绑定了 time 对象的类型。即此函数既要知道数据结构,同时又要对数据进行可视化处理。

提取出容器型组件

容器型组件知道数据及其结构,以及数据的来源。它们知道是如何运转的,或所谓的业务逻辑。它们接收信息并对其进行处理,以方便展示型组件使用。通常,我们使用 高阶组件 来创建容器型组件,因为它们为我们的自定义逻辑提供了缓冲区。

下面是 ClockContainer 的代码:

  1. // Clock/index.js
  2. import Clock from './Clock.jsx'; // <-- 展示型组件
  3. export default class ClockContainer extends React.Component {
  4. constructor(props) {
  5. super(props);
  6. this.state = { time: props.time };
  7. this._update = this._updateTime.bind(this);
  8. }
  9. render() {
  10. return <Clock { ...this._extract(this.state.time) }/>;
  11. }
  12. componentDidMount() {
  13. this._interval = setInterval(this._update, 1000);
  14. }
  15. componentWillUnmount() {
  16. clearInterval(this._interval);
  17. }
  18. _extract(time) {
  19. return {
  20. hours: time.getHours(),
  21. minutes: time.getMinutes(),
  22. seconds: time.getSeconds()
  23. };
  24. }
  25. _updateTime() {
  26. this.setState({
  27. time: new Date(this.state.time.getTime() + 1000)
  28. });
  29. }
  30. };

它接收 time (date 对象) 属性,使用 setInterval 循环并了解数据 (getHoursgetMinutesgetSeconds) 的详情。最后渲染展示型组件并传入时分秒三个数字。这里没有任何展示相关的内容。只有业务逻辑

展示型组件

展示型组件只涉及组件的外在展现形式。它们会有附加的 HTML 标记来使得页面更加漂亮。这种组件没有任何绑定及依赖。通常都是实现成 无状态组件,它们没有内部状态。

在本示例中,展示型组件只包含两位数的检查并返回 <h1> 标签:

  1. // Clock/Clock.jsx
  2. export default function Clock(props) {
  3. var [ hours, minutes, seconds ] = [
  4. props.hours,
  5. props.minutes,
  6. props.seconds
  7. ].map(num => num < 10 ? '0' + num : num);
  8. return <h1>{ hours } : { minutes } : { seconds }</h1>;
  9. };

好处

将组件分成容器型组件和展示型组件可以增加组件的可复用性。不改变时间或不使用 JavaScript Date 对象的应用中,都可以使用 Clock 函数/组件。原因是它相当单纯,不需要对所需数据的详情有任何了解。

容器型组件封装了逻辑,它们可以搭配不同的展示型组件使用,因为它们不参与任何展示相关的工作。我们上面所采用的方法是一个很好的示例,它阐明了容器型组件是如何不关心展示部分的内容的。我们可以很容易地从数字时钟切换到模拟时钟,唯一的变化就是替换 render 方法中的 <Clock> 组件。

测试也将变得更容易,因为组件承担的职责更少。容器型组件不关心 UI 。展示型组件只是纯粹地负责展示,它可以很好地预测出渲染后的 HTML 标记。

结语

容器型和展示型的并非是新概念,但是它真的非常适合 React 。它使得应用具有更好的结构,易于管理和扩展。