Skip to content

Files

Latest commit

c3011f5 · Jan 9, 2022

History

History
503 lines (352 loc) · 16.8 KB

File metadata and controls

503 lines (352 loc) · 16.8 KB

四、React 组件

用户界面在“反应”中被定义为组件。术语组件被许多其他框架使用。我们还可以使用 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")
  );

拥有hrefsrc属性的变量是这个组件可重用的原因。

注意我们是如何将组件定义为实际函数的。我们可以通过多种方式创建 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组件,它理解两个输入属性,hrefsrc。一旦我们准备好这个ClickableImage组件,我们就可以使用ReactDOM渲染功能将其安装在浏览器中。

代码清单 21:向 DOM 呈现一个反应组件

  ReactDOM.render(
    <ClickableImage href="google.com",
  src="google.com" />,
    document.getElementById("react")
  );

| | 注意:ReactDOM 是一个单独维护的库,可以与 React 一起使用来处理浏览器的 DOM。为了能够使用它,您需要在项目中包含它的 CDN 条目或导入它。这个 JSBin 模板有一个用 ReactDOM 安装的组件的工作示例。 |

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>
  );

反应。createClass

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调用的结果是一个组件实例。*