React 组件,元素和实例

December 18, 2015 by Dan Abramov

许多 React 初学者对组件、其实例以及元素之间的区别感到困惑。为什么有三个不同的术语来指代屏幕上绘制的内容?

管理实例

如果你不熟悉 React,那么此前你可能仅仅工作用到过组件类和实例。例如,你可能通过新建一个 class 来声明 Button 组件。当 app 运行起来以后,你可能会有若干个拥有自己属性和本地 state 的实例运行在屏幕上。这是传统的面向对象 UI 编程。那为什么要引入元素

在这些传统的 UI 模型中,需要由你来关心创建及销毁子组件们的实例。如果一个 Form 组件要渲染一个 Button 组件,那么它需要创建其实例并手动根据最新的信息使其保持同步。

class Form extends TraditionalObjectOrientedView {
  render() {
    // 读取一些数据到当前视图
    const { isSubmitted, buttonText } = this.attrs;

    if (!isSubmitted && !this.button) {
      // 表单还未提交。创建按钮!
      this.button = new Button({
        children: buttonText,
        color: 'blue'
      });
      this.el.appendChild(this.button.el);
    }

    if (this.button) {
      // 按钮可见。更新其文本!
      this.button.attrs.children = buttonText;
      this.button.render();
    }

    if (isSubmitted && this.button) {
      // 表单已提交。销毁按钮!
      this.el.removeChild(this.button.el);
      this.button.destroy();
    }

    if (isSubmitted && !this.message) {
      // 表达已经提交。显示成功信息!
      this.message = new Message({ text: 'Success!' });
      this.el.appendChild(this.message.el);
    }
  }
}

虽然这是一段伪代码,但只要你在使用一个库(比如 Backbone)编写复合界面代码的同时,恪守面向对象的思想,最终代码多少都会变成这个样子。

每个组件实例都必须保持对 DOM 节点和子组件实例的引用,同时在正确的时机新建、更新、销毁它们。随着组件可能状态的平方式增长,代码行数也将增长。与此同时,父组件直接访问子组件实例也使得将来它们彼此之间更难解耦。

所以 React 有何不同呢?

元素描述了树

这正是 React 希望元素施展拳脚之处。元素是一个用来描述组件实例或 DOM 节点及其需要属性的普通对象。它只包含组件类型(比如 Button),其属性(比如color)以及所有其下子元素的相关信息。

一个元素不是一个确切的实例。他是一种告诉 React 你想要在屏幕上看到什么的方法。你不能在元素上调用任何方法。它只是一个携有 type: (string | ReactClass)props: Object1 字段的不可变描述对象。

DOM 元素

当一个元素的 type 是字符串时,它代表了一个具有该标签名称的 DOM 节点。props 对应于它的属性。React 这就是 React 将呈现的内容。举个例子:

{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}

这个元素只不过是一种将下面这段 HTML 表示成一个普通对象的方法。

<button class='button button-blue'>
  <b>
    OK!
  </b>
</button>

注意元素是如何嵌套的。按照惯例,当我们要创建一棵 element tree 时,我们指定一或多个子元素作为其 children 成员。

重要的是子元素和父元素仅仅作为描述而不是真正的实例。当你创建了它们,它们并不代表任何屏幕上的东西。你可以创建、丢弃它们,不必担心什么。

React 元素易于遍历,无需解析。此外它们比起真实的 DOM 元素更轻——它们只是对象!

组件元素

然而,元素的 type 究竟是一个函数还是一个类则视 React 组件而定:

{
  type: Button,
  props: {
    color: 'blue',
    children: 'OK!'
  }
}

这是 React 的核心思想。

一个用于描述组件的元素也是一个元素,就像一个用于描述 DOM 节点的元素一样。它们可以彼此嵌套,互相混合。

该特性让你可以将 DangerButton 组件定义为一个被指定 color 值的 Button,而你完全不必关心 Button 是渲染成一个 DOM <button><div> 或其他东西。

const DangerButton = ({ children }) => ({
  type: Button,
  props: {
    color: 'red',
    children: children
  }
});

你可以混合和匹配 DOM 及组件元素在一个单独的 element tree 中:

const DeleteAccount = () => ({
  type: 'div',
  props: {
    children: [{
      type: 'p',
      props: {
        children: 'Are you sure?'
      }
    }, {
      type: DangerButton,
      props: {
        children: 'Yep'
      }
    }, {
      type: Button,
      props: {
        color: 'blue',
        children: 'Cancel'
      }
   }]
});

或者,如果你喜欢 JSX:

const DeleteAccount = () => (
  <div>
    <p>Are you sure?</p>
    <DangerButton>Yep</DangerButton>
    <Button color='blue'>Cancel</Button>
  </div>
);

这种混合和匹配有助于组件彼此分离,因为它们可以仅仅通过组合来表示 is-ahas-a 的关系:

  • Button 是一个被指定部分属性的 DOM <button>
  • DangerButton 是一个被指定部分属性的 Button
  • DeleteAccount 在一个 <div>中包含一个 Button 和一个 DangerButton

组件封装 Element Trees

当 React 遇到一个带有函数或类 type 的元素时,它知道要问那个组件它要呈现什么元素,并给出相应的 props

当它遇到这个元素:

{
  type: Button,
  props: {
    color: 'blue',
    children: 'OK!'
  }
}

React 将问 Button 它将渲染成什么。Button 将会返回这个元素:

{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}

React 将重复这个过程直到它知道了页面上每一个组件之下的 DOM 标签元素。

React 就像一个孩子。在搞清楚这个世界的每一件小事之前,它都要向每一个你所解释的 ”X是Y“ 询问 ”Y是什么“。

还记得之前 Form 的例子吗?它可以用 React 编写如下1

const Form = ({ isSubmitted, buttonText }) => {
  if (isSubmitted) {
    // Form 提交了!返回一个 message 元素。
    return {
      type: Message,
      props: {
        text: 'Success!'
      }
    };
  }

  // Form 还在继续显示!返回一个 button 元素。
  return {
    type: Button,
    props: {
      children: buttonText,
      color: 'blue'
    }
  };
};

这就是它!对于一个 React 组件,props 就是输入,element tree 就是输出。

返回的 element tree 可以包含描述 DOM 节点的元素,描述其他组件的元素。这使你可以组成 UI 的独立部分,而无需依赖其内部 DOM 结构。

我们让 React 创建,更新,销毁实例。我们通过组件返回的元素描述它们,而 React 负责管理这些实例。

组件可以是类或函数

在之前的代码中,Form, MessageButton 是 React 组件。它们既可以像此前那样被写作函数,也可以通过React.Component写作类。这三种声明组件的方式几乎是等效的:

// 1) 作为一个带 props 的函数
const Button = ({ children, color }) => ({
  type: 'button',
  props: {
    className: 'button button-' + color,
    children: {
      type: 'b',
      props: {
        children: children
      }
    }
  }
});

// 2) 使用 React.createClass() 工厂
const Button = React.createClass({
  render() {
    const { children, color } = this.props;
    return {
      type: 'button',
      props: {
        className: 'button button-' + color,
        children: {
          type: 'b',
          props: {
            children: children
          }
        }
      }
    };
  }
});

// 3) 作为从 React.Component 继承的ES6类
class Button extends React.Component {
  render() {
    const { children, color } = this.props;
    return {
      type: 'button',
      props: {
        className: 'button button-' + color,
        children: {
          type: 'b',
          props: {
            children: children
          }
        }
      }
    };
  }
}

当一个组件用类定义时,它会比一个函数组件要强大一点。当创建或销毁相应的 DOM 节点时,它能存储一些本地状态并执行自定义逻辑。

函数组件功能更弱,但它更简单。它就像一个只有 render() 方法的 class 组件。除非你需要只有从类组件那才能得到的功能,否则我们建议你用函数组件。

然而,不论函数或类,根本上来说它们都是 React 组件。它们将 props 作为输入,返回元素作为输出。

自上而下的的协调

当你调用:

ReactDOM.render({
  type: Form,
  props: {
    isSubmitted: false,
    buttonText: 'OK!'
  }
}, document.getElementById('root'));

React 先将那些 props 传入 Form 组件,随后等待返回 element tree。它将通过更简单的”基元“逐步完善对组件树的理解:

// React: 你告诉了我这...
{
  type: Form,
  props: {
    isSubmitted: false,
    buttonText: 'OK!'
  }
}

// React: ...然后 Form 告诉了我这...
{
  type: Button,
  props: {
    children: 'OK!',
    color: 'blue'
  }
}

// React: ...然后 Button 告诉了我这!我觉得我做完了。
{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}

这是 React 称之为协调过程的一部分。它开始于你调用 ReactDOM.render()setState()。在协调结束的时候,React 知道了结果的 DOM 树,然后一个渲染器像 react-domreact-native 尽可能使用最少的变化来更新 DOM 节点(或在 React Native 中的特定平台视图)。

这种逐步改善过程也是为什么 React 应用易于优化的原因。如果你组件树的一部分因为变得太大导致 React 无法高效的访问,你可以告诉它如果相关props没有变化,跳过这个”改善“并且只比对树的某些部分。如果 props 是不可变的,那么计算其是否发生变化是非常快的,因此 React 和不可变性可以很好地协同工作,并且可以以最小的努力提供出色的优化。

你可能已经注意到,此博客文章讨论了很多组件和元素的内容,没有提到太多实例。事实上,与绝大多数面向对象 UI 框架相比,实例在 React 中的重要性要小很多。

只有使用类声明的组件有实例,而且你永远不会直接创建它们:React 为你做了那些。尽管存在父组件实例访问子组件实例的机制,但只要不是万不得已(譬如为某个字段设置聚焦),我么通常都应该避免这种操作。

React 负责为每个类组件创建一个实例,所以你可以用面向对象的方法写一个带有方法和本地状态的组件。但除此之外,在 React 的编程模型中实例并不十分重要,而且这些都由它自己管理。

总结

Summary

一个元素是一个普通的对象。它被用来描述什么需要在屏幕上显示,根据 DOM 节点还是其他组件。元素可以在它们的 props 里包含其他元素。创建一个 React 元素很廉价。一旦一个元素被创建了,它就不再改变。

一个组件可以通过多种不同的方式声明。它可以是一个带有 render() 方法的类。或者,更简单些,它可以被定义成一个函数。不论何种方式,它都需要 props 作为输入,返回一个 element tree 作为输出。

当一个组件收到一些 props 作为输入,其必是因为某个父组件返回了一个带有它 type 和这些 props 的元素。这就是为什么在 React 中人们说 props 是单向流:从父级到子级。

实例是你在编写组件类中称为this的东西。它在保存本地状态和响应生命周期事件上很有用。

函数组件根本没有实例。类组件才有实例,但你永远不需要去直接创建组件实例——React 会负责这些。

最后,要新建一个元素,使用React.createElement()JSX, 或 element factory helper。不要在真实代码中将元素写作普通对象——知道它们是处于底层的普通对象足矣。

拓展阅读


  1. 出于安全原因,所有 React 元素都需要在对象上声明一个额外的 $$typeof: Symbol.for('react.element') 字段。在上文的示例中将其省略了。为了使你理解底层发生了什么,这篇博客为元素使用了内联对象,但无论是为元素添加 $$typeof 还是更改代码使用 React.createElement() 或 JSX,代码都不会如预期运行。

Is this page useful?编辑此页面