第一个组件

上一节,我们写了一个页面组件。

export default () => {
  return <div>hello world</div>;
}

页面插入这个组件以后,就会显示 Hello World 。但是,上一节并没有解释这段代码的含义,只是让大家照着拷贝。

本节就来解释什么是组件,以及怎样写组件。这是 React 和 Ant Design 的使用基础,只有学会了这些内容,才能理解后面的知识。如果你对 React 已经有所了解,可以跳过这一节。

组件是什么?

按照功能划分,一张网页可以由多个互相独立的功能单位组成,这种功能单位就叫做“组件”(component)。比如,典型的网页分成页头、内容、页尾三个部分,就可以写成三个组件:Header、Content、Footer。这些组件拼装在一起,就构成了一张页面。

组件内部还可以包含下一级的组件。比如,“文章”组件内部可以包含“表单”组件,“表单”组件内部又可以包含“按钮”组件。

组件的好处有很多,下面是其中几点。

  • 有利于细化 UI 逻辑,不同的组件负责不同的功能点。
  • 有利于代码复用,多个页面可以使用同样的组件。
  • 有利于人员分工,不同的工程师负责不同的组件。

React 的核心概念就是组件。这个框架的主要功能,就是定义了一套编写和使用组件的规范。本节开头的那段代码,就是定义了一个最简单的组件。

export default () => {
  return <div>hello world</div>;
}

上面代码采用 ES6 模块格式,默认输出一个箭头函数。这个函数执行后,返回了一段 JSX 代码(后文介绍),代表 hello world 区块。这就是最简单的 React 组件的写法。

JSX 语法

第一次接触 React 的用户,都会有一个共同的疑问,为什么 JavaScript 代码里面可以直接插入 HTML 代码。这难道不会报错吗?

回答是如果你把上面的代码,放到 JavaScript 引擎里面运行,确实会报错。因为这种语法不是 JavaScript,而是 React 为了方便开发者自创的 JSX 语法。

JSX 可以被 Babel 转码器转为正常的 JavaScript 语法。上面的 JSX 语法转码后的结果如下。

exports.default = function () {
  return React.createElement(
    "div",
    null,
    "hello world"
  );
};

两种写法一比较,就会发现对于复杂的 UI 组件来说,JSX 更易写也更易读。所以,几乎所有的 React 开发者都使用 JSX 语法。

JSX 语法的特点就是,凡是使用 JavaScript 的值的地方,都可以插入这种类似 HTML 的语法。

const element = <h1>Hello, world!</h1>;

这里有两个注意点。一是所有 HTML 标签必须是闭合的,如果写成<h1>Hello就会报错。如果是那种没有闭合语法的标签,必须在标签尾部加上斜杠,比如<img src="" />。二是任何 JSX 表达式,顶层只能有一个标签,也就是说只能有一个根元素。下面的写法会报错。

// 报错
const element = <h1>hello</h1><h1>world</h1>;

// 不报错
const element = <div><h1>hello</h1><h1>world</h1></div>;

上面代码中,第一种写法会报错,因为根元素的位置有两个并列的<h1>标签。在它们外面再包一层,就不会报错了。

一般来说,HTML 原生标签都使用小写,开发者自定义的组件标签首字母大写,比如<MyComponent/>

JSX 语法允许 HTML 与 JS 代码混写。

const element = (
  <h1>
    Hello, {formatName(user)}!
  </h1>
);

上面代码中,<h1>标签的文字内容部分嵌入了 JS 代码。每次生成的文本,取决于函数formatName(user)执行的结果。

可以看到,JSX 语法的值的部分,只要使用了大括号{},就表示进入 JS 的上下文,可以写入 JS 代码。

更多介绍请参考官方文档

React 组件语法

虽然输出 JSX 代码的函数就是一个 React 组件,但是这种写法只适合那些最简单的组件。更正式、更通用的组件写法,要使用 ES6 类(class)的语法。

import React from 'react';

class ShoppingList extends React.Component {
  render() {
    return (
      <div className="shopping-list">
        <h1>Shopping List for {this.props.name}</h1>
        <ul>
          <li>Instagram</li>
          <li>WhatsApp</li>
          <li>Oculus</li>
        </ul>
      </div>
    );
  }
}

export default ShoppingList;

上面代码定义了一个 ShoppingList 组件。自定义的组件必须继承React.Component这个基类,然后必须有一个render方法,给出组件的输出。

使用 React 组件也很简单,引入这个组件以后,就可以直接使用。假定上面的组件脚本叫做shoppinglist.js,那么使用它的代码如下。

import React from 'React';
import ShoppingList from './shoppinglist.js';

class Content extends React.Component {
  render() {
    return (
      <ShoppingList name="张三" />
    );
  }
}

export default Content;

上面代码中,我们新建了一个Content组件,里面使用了ShoppingList组件。注意,由于这个组件除了name参数,没有其他内容,所以可以写成<ShoppingList name="张三"/>这种直接闭合的形式。否则,可以写成下面的形式。

class Content extends React.Component {
  render() {
    return (
      <ShoppingList name="张三">
        {/* 插入的其他内容 */}
      </ShoppingList>
    );
  }
}

组件的参数

上一节的<ShoppingList name="张三"/>这行代码,ShoppingList是组件名,name="张三"表示这个组件的有一个name参数,值为张三

组件内部,所有参数都放在this.props属性上面。通过this.props.name就可以拿到传入的值(张三)。

<h1>Shopping List for {this.props.name}</h1>

通过这种参数机制,React 组件可以接受外部消息。

this.props对象有一个非常特殊的参数this.props.children,表示当前组件“包裹”的所有内容。比如,上面代码里面的Shopping List for {this.props.name},就是<h1>元素的this.props.children。这个属性在 React 里面有很大的作用,它意味着组件内部可以拿到,用户在组件里面放置的内容。

我们来看一个例子。下面是一个组件,内部使用props.children,获取用户传入的内容。

const Picture = (props) => {
  return (
    <div>
      <img src={props.src} />
      {props.children}
    </div>
  )
}

下面就是使用时,怎么向props.children传入内容。

render () {
  const picture = {
    src: 'https://cdn.nlark.com/yuque/0/2018/jpeg/84141/1536207007004-59352a41-4ad8-409b-a416-a4f324eb6d0b.jpeg',
  };
  return (
    <div className='container'>
      <Picture src={picture.src}>
        // 这里放置的内容就是 props.children
      </Picture>
    </div>
  )
}

组件的状态

除了接受外部参数,组件内部也有不同的状态。React 规定,组件的内部状态记录在this.state这个对象上面。

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button
        className="square"
        onClick={() => this.setState({value: 'X'})}
      >
        {this.state.value}
      </button>
    );
  }
}

上面代码中,组件Square的构造方法constructor里面定义了当前状态this.state对象。Square 组件的这个对象只有一个value属性,一开始的值是null

用户点击按钮以后,onClick监听函数执行this.setState()方法。React 使用这个方法,更新this.state对象。这个方法有一个特点,就是每次执行以后,它会自动调用render方法,导致 UI 更新。UI 里面使用this.state.value,输出状态值。随着用户点击按钮,页面就会显示X

可以看到,这个例子里面,内部状态用来区分用户是否点击了按钮。

生命周期方法

组件的运行过程中,存在不同的阶段。React 为这些阶段提供了钩子方法,允许开发者自定义每个阶段自动执行的函数。这些方法统称为生命周期方法(lifecycle methods)。

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {

  }

  componentWillUnmount() {

  }

  componentDidUpdate() {
  
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

上面代码中,componentDidMount()componentWillUnmount()componentDidUpdate()就是三个最常用的生命周期方法。其中,componentDidMount()会在组件挂载后自动调用,componentWillUnmount()会在组件卸载前自动调用,componentDidUpdate()会在 UI 每次更新后调用(即组件挂载成功以后,每次调用 render 方法,都会触发这个方法)。

后面的章节结合实例,会对这几个方法有更详细的介绍。

此外,还有三个生命周期方法,不是经常使用,这里只需要简单了解。

  • shouldComponentUpdate(nextProps, nextState):每当this.propsthis.state有变化,在render方法执行之前,就会调用这个方法。该方法返回一个布尔值,表示是否应该继续执行render方法,即如果返回false,UI 就不会更新,默认返回true。组件挂载时,render方法的第一次执行,不会调用这个方法。
  • static getDerivedStateFromProps(props, state):该方法在render方法执行之前调用,包括组件的第一次记载。它应该返回一个新的 state 对象,通常用在组件状态依赖外部输入的参数的情况。
  • getSnapshotBeforeUpdate():该方法在每次 DOM 更新之前调用,用来收集 DOM 信息。它返回的值,将作为参数传入componentDidUpdate()方法。