超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

React - Components, Elements, and Instances

组件(Components)、元素(Elements)和实例(Instances)之间的区别很容易被混淆,本文主要介绍了这几个概念的区别并详细说明了 React 管理 DOM 的思路。

传统组件自己管理实例(Managing the Instances)

在传统的面向对象的 UI 编程中,你需要自己手动创建和管理各种组件的实例对象,比如,一个 Form 组件想要渲染一个 Button 子组件,需要实例化这个 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);
}
}
}

这只是一个伪代码,但是当使用类似 Backbone 的库以面向对象方式编写复合 UI 代码时,大概的结构就是这个样子。

每一个组件实例都需要保留它的 DOM 节点引用和子组件的实例,炳轩仔在合适的时机去创建、更新、删除这些实例。代码行数会随着组件的状态数量,以几何的速度增长。而且组件还需要访问它的子组件实例,这样耦合度非常高,难以解耦。

React 则采用了不同的方式:

React 使用元素描述节点树(Elements Describe the Tree)

react 中使用“元素”描述了节点树:

An element is a plain object describing a component instance or DOM node and its desired properties.
元素是一个简单对象(plain object),它描述了组件的示例或者DOM 节点以及它们所需要的属性。元素仅包含组件的类型、组件的属性和它的子元素或子组件。

元素不是实际存在的示例,相反,它用于告诉 react 想要展示在屏幕上的内容,不能在元素上调用任何方法,它仅仅是一个不可变的描述对象,包含两个字段:type: (string | ReactClass)props: Object

DOM 元素(DOM Elements)

当元素的 type 字段值类型是字符串的时候,它描述了一个这个类型的 DOM 节点(此时 type 的值就是一个 tagName ,比如是 div), props 则是这个节点的属性,如下元素:

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

这个元素描述了如下的 HTML 结构:

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

注意元素是怎么嵌套的,当我们想要创建一个元素树的时候,指定一个或者多个元素作为容器的子元素。

需要知道的是这些元素仅仅是对内容的一种描述,并不是实际存在的实例,当它们被创建的时候,并不会被饮用任何屏幕上的的东西,你可以任意的创建和删除它们,不会对屏幕上的内容造成任何的影响。

React 元素非常容易遍历,不需要解析,而且比实际的 DOM 元素轻量的多,它们只是一些 plain object

组件元素(Component Elements)

元素的 type 字段值类型可以是一个 React 组件相关联的函数或者类:

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

这就是 react 的核心观念:

An element describing a component is also an element, just like an element describing the DOM node. They can be nested and mixed with each other.
一个描述组件的元素依然是元素,和描述 DOM 的元素没有任何区别,它们可以被互相嵌套和混合。

这个特性允许你定义一个 DangerButton 组件作为一个有特定 color 属性的 Button 组件,而不需要关心 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>
);

这种混合搭配有助于保持组件之间的解耦,它们可以仅通过组合(composition)就可以表达组件间的 is-ahas-a 关系:

  • Button 是(is-a)附带特定参数的 <button>
  • DangerButton 是(is-a)附带特定参数的 Button 组件
  • DeleteAccount 是在 <div> 里包含(has-aButtonDangerButton

组件封装元素树(Components Encapsulate Element Trees)

React 遇到一个 type 属性是函数或者类的元素, React 就会把 props 传递给这个组件,并获取这个组件返回的元素,如下示例:

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

Reactprops 属性传递给 Button 组件,Button 组件返回它的元素:

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

React 会不断重复这个过程,知道它确切的了解了所有的组件对应的底层 DOM 元素,如最开始的 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 组件而言, props 是输入,而元素或者元素树则是输出

被组件返回的元素树可能包含描述 DOM 节点和描述其他组件的子元素,这可以让我们更自由的组合 UI 而不需要依赖它们内部的结构。

也就是说,React 会替我们完成实例的创建、更新和销毁的实例管理的工作,我们只需要使用组件返回元素来描述它们。

组件可以是类或者函数(Components Can Be Classes or Functions)

在上述的示例中,FormMessageButton 都是 React 组件,React 组件有多种创建方式:写成函数、继承 React.Component 、 使用 React.createClass,这三种方式几乎一样:

1.使用函数创建组件:

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.创建一个 ES6 的类,继承 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 被创建或删除的时候执行一些操作。

但是,函数组件虽然没有类组件强大,但是更加简洁,当不需要维护内部状态和声明周期的操作时,桁架建议使用函数组件。

不管是函数组件或者类组件,他们都是 React 组件,都以 Props 作为输入,以元素作为输出。

自上而下的和解(Top-Down Reconciliation)

当调用:

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

React 会提供这些 propsForm 组件,并获取 Form 返回的元素树,React 会以“更简单的原语”(simpler primitives)简化对你的元素树的理解,这个过程如下:

1.我们给 React 这个结构:

{
type: Form,
props: {
isSubmitted: false,
buttonText: 'OK!'
}
}

2.ReactForm 组件查询元素树, Form 返回以下:

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

3.ReactButton 组件查询元素树, Button 返回以下,到了最底层结束:

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

这个过程是被 React 称为 Reconciliation 过程的一部分,当你调用 ReactDOM.render() 或者 setState() 的时候会被执行。在这个过程结束后,React 知道了 DOM 树的结果,然后使用如 react-domreact-nativerenderer 根据应用必需的最小的变更内容更新``DOM视图(或者是React Native` 的特定平台视图)。

这种渐进式的“精炼”(refining)过程正是 React 应用很容易优化的原因,如果组件树的某一部分变得非常大的情况下,可以让 React 跳过这部分的“精炼”以及 diff 的过程(使用 shouldComponentUpdate ),如果 props 是不可更改的(immutable),比较它们的变化会非常的快,这也是 Reactimmutability 结合非常好的原因,并且可以使用最小的代价获得最大的优化。

你可能注意到这篇文章讲了大量的关于组件和元素的内容,但是并没有提到太多关于实例的内容。这是因为,和传统的面向对象的 UI 框架相比较,实例在 React 中并没有那么的重要。

只有当组件声明为类的时候才会拥有实例,但是从来不需要直接创建它的实例,React 会帮你创建和管理它的实例。只有在一些特殊的情况下,组件需要 访问实例 执行操作,比如,为一个表单元素获取焦点,而且这种行为通常是需要避免的。

总结(Summary)

元素是一个简单对象,用于描述你想通过 DOM 节点或者其他组件展示在屏幕上的内容。元素可以在它们的属性中包含其他的元素。创建一个 React 元素的成本非常的低,一个元素一旦被创建,将不能修改。

实例是在组件上使用 this 获得的,它在存储本地状态和执行生命周期的事件非常有用。

函数式组件并不存在实例,类组件存在有实例,但是通常不需要我们管理,有 React 替我们管理这些实例。

创建 React 元素有 React.createElement()React.createFactory()JSX、函数式几种方法,不要在代码里用对象来写元素,只需要知道元素的本质是对象即可。

注意 出于安全的考虑,所有的 React 元素都需要声明一个额外的 $$typeof: Symbol.for('react.element') 字段,这个字段在上述的例子中都被忽略了。这边文章主要用于介绍上述概念,所以除非添加该字段或者使用 React.createElementJSX 去修改上面的代码,否则上面的代码并不能正常的运行。

参考

本文主要翻译了 React 的官方文档: React Components, Elements, and Instances,并根据个人理解和习惯稍有调整。