剖析React内部运行机制-「译」React组件、元素和实例

2,542 阅读9分钟

原文: React Components, Elements, and Instances


组件与它们实例以及元素之间的区别困扰着很多的React初学者。为什么会有三种不同的术语来指代屏幕上的东西?


管理实例

如果你是一个React新手,你以前可能只使用过组件类和实例。比如,你通过创建一个class来声明一个Button。app执行过程中,在屏幕中你会得到这个组件的几个实例,每一个都有自己的属性和本地状态。这是传统的面向对象编程。那么为什么要介绍元素呢?

在传统的UI模式中,你需要自己去创建和销毁子组件实例。如果一个Form组件想要渲染一个Button组件,那么它需要创建自己的实例,并且手动保持它处于最新信息的状态。

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

上面是Form组件的伪代码,但是这些或多或少是你编写组合UI代码的最终结果,这些代码使用一个库比如BackBone,以面向对象的方式达到行为一致性。

每个组件实例必须保持对其DOM节点和子组件实例的引用,并在适当的时候创建、更新和销毁它们。代码行数将会朝着组件可能状态数的平方量级增长,并且父组件可以直接访问其子组件实例,这使得将来很难解耦它们。

那么,React有什么不同呢?

用元素来描述树

在React中,这就是元素发挥作用的地方。元素是描述组件实例或DOM节点及其所需属性的普通对象。 它只包含关于组件类型(例如,按钮)、属性(例如,颜色)和其中一些子元素的信息。

元素不是真实的实例,而是一种告诉React你想在屏幕上看到什么的方式。我们不能调用元素上面的任何方法,因为它只是一个不可变的描述对象,有两个字段:type: (string | ReactClass)props: object

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

这个元素只是将下列HTML表示为普通对象的一种方式:

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

请注意元素是如何被嵌套的。按照惯例,在创建元素树时,我们指定一个或多个子元素作为其包含元素的children属性。

重要的是,子元素和父元素都只是描述,而不是真实的实例。当你创建它们时,它们不会指向屏幕上的任何东西。你可以创造它们,然后把它们扔掉,这并不会产生其他影响。

React元素很容易遍历,不需要解析,并且它们比实际的DOM元素轻量得多——因为它们只是对象!

组件元素

事实上,元素的类型(type)也可以是对应于React组件的函数(function)或类(class),就像下面这样:

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

这是React的核心理念。

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

这个特点让你定义一个DangerButton组件为一个Button,可以具有特定的color属性,而不需要担心Button渲染成一个DOM < Button >,一个<div>,或其他元素。

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

你可以在一个元素树中组会使用DOM元素和组件元素:

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

这种混合和匹配有助于保持组件之间的解耦,因为它们可以仅通过组合来表达“继承”和“从属”关系:

  • Button是一个具有特定属性的DOM < Button >
  • DangerButton是一个具有特定属性的Button
  • DeleteAccountdiv中包含了一个Button和一个DangerButton

组件封装元素树

当React看到一个带有函数(function)或类(class)类型的元素时,它知道询问该组件应该渲染哪个元素,并给出相应的属性。

当React看到下面这个元素时:

{
  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写成下面这样:

const Form = ({ isSubmitted, buttonText }) => {
  if (isSubmitted) {
    // Form submitted! Return a message element.
    return {
      type: Message,
      props: {
        text: 'Success!'
      }
    };
  }

  // Form is still visible! Return a button element.
  return {
    type: Button,
    props: {
      children: buttonText,
      color: 'blue'
    }
  };
};

就是这样。对于React组件来说,输入是属性,输出是元素树。

返回的元素树可以包含描述DOM节点的元素和描述其他组件的元素。这使你可以组合相互独立的UI部分,而不依赖于它们的内部DOM结构。

我们使用React创建、更新和销毁实例,用从组件返回的元素来描述它们,而React负责管理这些实例。

组件可以是类(class)或函数(function)

在上面的代码中,Form, Message, 和 Button都是React组件。它们既可以被写成函数形式就像上面代码一样,也可以写成类的形式继承自React.Component。这三种声明组件的方式是等效的。

// 1) As a function of props
const Button = ({ children, color }) => ({
  type: 'button',
  props: {
    className: 'button button-' + color,
    children: {
      type: 'b',
      props: {
        children: children
      }
    }
  }
});

// 2) Using the React.createClass() factory
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) As an ES6 class descending from React.Component
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()方法。除非您需要只在类中可用的特性,否则我们鼓励您使用函数组件。

然而,无论是函数组件还是类组件,它们本质上都是React的组件。它们将属性作为输入,并将元素作为输出返回。

自上而下的协调算法

当你调用:

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

React将询问Form组件它根据这些属性返回什么元素树。它将逐步“细化”它对组件树的理解,以得到更简单的术语:

// React: You told me this...
{
  type: Form,
  props: {
    isSubmitted: false,
    buttonText: 'OK!'
  }
}

// React: ...And Form told me this...
{
  type: Button,
  props: {
    children: 'OK!',
    color: 'blue'
  }
}

// React: ...and Button told me this! I guess I'm done.
{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}

这是React调用协调算法过程的一部分,它是在调用ReactDOM.render()setState()时被触发执行。在协调结束时,React知道结果DOM树,而像react-dom或react-native这样的渲染器会使用所需的最小变化集合更新到DOM节点(在React Native的情况下,会应用特定于平台的视图)。

这种渐进的解析过程也是React应用程序易于优化的原因所在。如果组件树的某些部分太大,以致React无法有效访问。如果相关的属性没有改变,你可以告诉它跳过这个“细节”只diffing组件树的某些部分。如果这些属性是不可变的,那么计算它们是否改变是非常快的,所以React和immutability可以很好地协同工作,并且可以用最小的努力提供最佳优化。

您可能已经注意到,这篇博客文章主要讨论了组件和元素,而不是实例。事实是,与大多数面向对象的UI框架相比,React中的实例的重要性要小得多。

只有声明为类的组件才有实例,而且你永远不会直接创建它们,React为你做这些。由于父组件实例访问子组件实例机制已经存在,它们只用于必要的操作(例如设置字段的焦点),通常应该避免使用。

React负责为每个类组件创建一个实例,因此可以使用方法和本地状态以面向对象的方式编写组件,但除此之外,实例在React的编程模型中不是很重要,由React自己管理。

总结

元素是一个普通对象,它根据DOM节点或其他组件描述希望在屏幕上显示的内容。元素可以在其属性中包含其他元素。创建一个React元素很容易。一旦创建了一个元素,它就不会发生改变。

组件可以用几种不同的方式声明。它可以是一个带有render()方法的类。或者在简单的情况下,可以将它定义为一个函数。在这两种情况下,它都接受属性作为输入,并返回一个元素树作为输出。

当一个组件接收到一些属性作为输入时,它就变成了一个特定的父组件,返回一个元素及其类型和这些属性。这就是为什么人们说这些属性是单向流动的:从父到子。

实例就是在编写的组件类中引用为this的实例。它对于存储本地状态和响应生命周期事件非常有用。

函数组件没有实例。类组件有实例,但你不需要直接创建组件实例—react负责这方面的工作。

最后,创建元素可以使用React.createElement(), JSX或者element factory helper.不要在实际代码中将元素编写为普通对象—要知道它们是隐藏在底层的普通对象。

拓展阅读

出于安全因素,所有的React元素对象需要一个额外的字段 - ?typeof: Symbol.for('react.element')声明。这是上文中遗漏的地方。本篇文章使用元素的内联对象来让你了解底层发生了什么,但是除非你向元素添加?typeof,或者更改代码以使用react. createElement()JSX,否则代码不会按预设的逻辑运行。