React 中 Components, Elements 和 Instances 的区别

注:本文内容部分翻译自 React Components, Elements, and Instances,部分源自自己的理解。

在使用 React 时,我们常常会遇到 Components、Elements 与 Instances 三个概念,并且这三者在 React 语境中有明确指向的。熟悉这三个概念之后,我们才能明白当我们在写 JSX 时,我们在写什么。

Instances

传统面向对象的 UI 组件的 instance

在 React 出现之前,构建页面时我们也会使用面向对象的思想来创建一个类来声明一个 Component,当程序运行时,页面上可能有多个同个组件的 instance,每个 instance 都有自己的属性和 local state。

在传统 UI 模型中,用户必须自己来创建和销毁子组件的实例,就像这个 Form 组件一样:

class Form extends TraditionalObjectOrientedView {
  render() {
    // Read some data passed to the view
    const { isSubmitted, buttonText } = this.attrs;
    if (!isSubmitted && !this.button) {
      // Form is not yet submitted. Create the button!
      this.button = new Button({
        children: buttonText,
        color: 'blue'
      });
      this.el.appendChild(this.button.el);
    }
    if (this.button) {
      // The button is visible. Update its text!
      this.button.attrs.children = buttonText;
      this.button.render();
    }
    if (isSubmitted && this.button) {
      // Form was submitted. Destroy the button!
      this.el.removeChild(this.button.el);
      this.button.destroy();
    }
    if (isSubmitted && !this.message) {
      // Form was submitted. Show the success message!
      this.message = new Message({ text: 'Success!' });
      this.el.appendChild(this.message.el);
    }
  }
}

每个组件都必须保存自己的 DOM 节点和子组件实例的引用,并在合适的时间创建、更新和销毁它们。这种 UI 编写方式存在两个问题:

  1. 代码量会随着组件可能存在的状态的数量的平方倍数增长。
  2. 父组件可以直接访问子组件会使解耦变得很困难。

在 React 中,这些问题可以被轻松解决。

React 中的组件 instance

相比其他面向对象的 UI 框架,在 React 中 instances 的概念并不是十分重要,因为我们只需通过 Elements 来描述界面 UI 或者声明 Component,React 自己会管理 Component 的 instance。

这里需要注意的是,由于 functional component 是纯函数,它们并没有实例。它们只接收 props并返回一棵 Virtual DOM 树,没有 lifecycle 与 local state。

Elements

在 React 中,一个 Element 就是描述了一个组件实例或 DOM 节点及其属性的 plain object。它仅包含有关组件类型,其属性(如 color )以及其中的子元素的信息。它的作用是用来向 React 描述开发者想在页面上 render 什么东西。

我们可以看看React.createElement的源码来探索 Element 的组成:

/**
 * Factory method to create a new React element. This no longer adheres to
 * the class pattern, so do not use new to call it. Also, no instanceof check
 * will work. Instead test $$typeof field against Symbol.for('react.element') to check
 * if something is a React Element.
 *
 * @param {*} type
 * @param {*} key
 * @param {string|object} ref
 * @param {*} self A *temporary* helper to detect places where `this` is
 * different from the `owner` when React.createElement is called, so that we
 * can warn. We want to get rid of owner and replace string `ref`s with arrow
 * functions, and as long as `this` and owner are the same, there will be no
 * change in behavior.
 * @param {*} source An annotation object (added by a transpiler or otherwise)
 * indicating filename, line number, and/or other information.
 * @param {*} owner
 * @param {*} props
 * @internal
 */
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // This tag allows us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };
  // ...
  return element;
};


/**
 * Create and return a new ReactElement of the given type.
 * See https://reactjs.org/docs/react-api.html#createelement
 */
export function createElement(type, config, children) {
  // ...
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

从中可以看到,一个 Element 的组成大概是什么样子。

Element 的 type 可以是 string , 也可以是 function| class

type 为 string ,这个 Element 代表一个 DOM 节点:

现在如果我们使用 JSX 来在render()中 return 一个如下的 DOM 结构 :

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

那么 React 就会执行这样一个操作:

React.createElement('button', { className: 'button button-blue' },
	React.createElement('b', null, 'OK!')                
);

返回的 Element 对象是这样的:

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

type 为 function|class,这个 Element 代表一个 React component:

比如将上例的 button 封装成 functional component:

const Button = ({ children }) => (
  <button className='button button-blue'>
    <b>
      {children}
    </b>
  </button>
);

ReactDOM.render({
  <Button>
    OK!
  </Button>
}, document.getElementById('root'));

根据这个 Component,React 会逐渐知道需要构建怎样的一棵 element tree。

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

接着,Button 又告诉 React 接下来要返回怎样的 Element 对象:

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

React 会重复这个「询问」过程,直到它知道页面上每个组件的底层 DOM 元素。

一个描述 Component 的 Element 和描述 DOM 节点的元素没什么区别,他们可以相互嵌套和混合。这是 React 的一个核心概念。

这个特性可以让 DangerButton 组件定义为具有特定颜色属性值的 Button,而无需担心 Button 是否呈现为buttton,div或其他内容,实现了组件间的解耦。

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

与其他 Component 或 Element 组合使用:

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

Components

Components 是可重用的代码片段,它返回 React Element 以呈现给页面。

在 React 中,Component 有两种形式:

  1. class component
  2. functional component

前者相对后者而言,可以拥有 local state 以及可以在组件生命周期的各个阶段执行相应的逻辑。

当然,无论是 class component 还是 functional component,它们都会将 props 作为输入,并将 Elements 作为输出。

Top-down Reconciliation

当一个 Element 的 type 为 function|class时,React 不断「询问」直到它知道页面上每个组件的底层 DOM 元素的过程被称为 reconciliation 。每当调用ReactDOM.render()setState(), React 就会开始 reconciliation ,直到得到一棵最终 DOM 树。最后,ReactDOM 再以最小的代价根据这棵 DOM 树来更新真实 DOM 树。

总结

Element 在 React 中是一个很重要的概念。要创建 Elements,可以使用 React.createElement(),JSX 或 ReactElement工厂函数。当然,最方便的当然是 JSX 语法了!至此,愈发地觉得用 JSX 写 UI 真是太优雅了。

另外,要多思考「当我们在 XXX 时,我们在干什么」这个问题……

Show Comments