组件(Components
)、元素(Elements
)和实例(Instances
)之间的区别很容易被混淆,本文主要介绍了这几个概念的区别并详细说明了 React
管理 DOM
的思路。
传统组件自己管理实例(Managing the Instances)
在传统的面向对象的 UI
编程中,你需要自己手动创建和管理各种组件的实例对象,比如,一个 Form
组件想要渲染一个 Button
子组件,需要实例化这个 Button
子组件,并且手动更新他们的内容:
class Form extends TraditionalObjectOrientedView { |
这只是一个伪代码,但是当使用类似 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
则是这个节点的属性,如下元素:
{ |
这个元素描述了如下的 HTML
结构:
<button class='button button-blue'> |
注意元素是怎么嵌套的,当我们想要创建一个元素树的时候,指定一个或者多个元素作为容器的子元素。
需要知道的是这些元素仅仅是对内容的一种描述,并不是实际存在的实例,当它们被创建的时候,并不会被饮用任何屏幕上的的东西,你可以任意的创建和删除它们,不会对屏幕上的内容造成任何的影响。
React
元素非常容易遍历,不需要解析,而且比实际的 DOM
元素轻量的多,它们只是一些 plain object
。
组件元素(Component Elements)
元素的 type
字段值类型可以是一个 React
组件相关联的函数或者类:
{ |
这就是 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 }) => ({ |
同时,你可以混搭 DOM
元素和组件元素在一个元素树中:
const DeleteAccount = () => ({ |
或者,可以使用 jsx :
const DeleteAccount = () => ( |
这种混合搭配有助于保持组件之间的解耦,它们可以仅通过组合(composition
)就可以表达组件间的 is-a
和 has-a
关系:
Button
是(is-a
)附带特定参数的<button>
DangerButton
是(is-a
)附带特定参数的Button
组件DeleteAccount
是在<div>
里包含(has-a
)Button
和DangerButton
组件封装元素树(Components Encapsulate Element Trees)
当 React
遇到一个 type
属性是函数或者类的元素, React
就会把 props
传递给这个组件,并获取这个组件返回的元素,如下示例:
{ |
React
把 props
属性传递给 Button
组件,Button
组件返回它的元素:
{ |
React
会不断重复这个过程,知道它确切的了解了所有的组件对应的底层 DOM
元素,如最开始的 Form
示例,使用 React
可以被写成这个形式:
const Form = ({ isSubmitted, buttonText }) => { |
就是这样,对于 React
组件而言, props 是输入,而元素或者元素树则是输出。
被组件返回的元素树可能包含描述 DOM
节点和描述其他组件的子元素,这可以让我们更自由的组合 UI
而不需要依赖它们内部的结构。
也就是说,React
会替我们完成实例的创建、更新和销毁的实例管理的工作,我们只需要使用组件返回元素来描述它们。
组件可以是类或者函数(Components Can Be Classes or Functions)
在上述的示例中,Form
,Message
和 Button
都是 React
组件,React
组件有多种创建方式:写成函数、继承 React.Component
、 使用 React.createClass
,这三种方式几乎一样:
1.使用函数创建组件:
const Button = ({ children, color }) => ({ |
2.使用 React.createClass()
工厂方法:
const Button = React.createClass({ |
3.创建一个 ES6
的类,继承 React.Component
:
class Button extends React.Component { |
当然,当组件被定义为类的时候,回避当一位函数方法更加的强大,它可以存储一些本地状态,以及执行一些生命周期中的操作,比如在 DOM
被创建或删除的时候执行一些操作。
但是,函数组件虽然没有类组件强大,但是更加简洁,当不需要维护内部状态和声明周期的操作时,桁架建议使用函数组件。
不管是函数组件或者类组件,他们都是 React
组件,都以 Props
作为输入,以元素作为输出。
自上而下的和解(Top-Down Reconciliation)
当调用:
ReactDOM.render({ |
React
会提供这些 props
给 Form
组件,并获取 Form
返回的元素树,React
会以“更简单的原语”(simpler primitives
)简化对你的元素树的理解,这个过程如下:
1.我们给 React
这个结构:
{ |
2.React
向 Form
组件查询元素树, Form
返回以下:
{ |
3.React
向 Button
组件查询元素树, Button
返回以下,到了最底层结束:
{ |
这个过程是被 React
称为 Reconciliation
过程的一部分,当你调用 ReactDOM.render()
或者 setState()
的时候会被执行。在这个过程结束后,React
知道了 DOM
树的结果,然后使用如 react-dom
或 react-native
的 renderer
根据应用必需的最小的变更内容更新``DOM视图(或者是
React Native` 的特定平台视图)。
这种渐进式的“精炼”(refining
)过程正是 React
应用很容易优化的原因,如果组件树的某一部分变得非常大的情况下,可以让 React
跳过这部分的“精炼”以及 diff
的过程(使用 shouldComponentUpdate
),如果 props
是不可更改的(immutable
),比较它们的变化会非常的快,这也是 React
和 immutability
结合非常好的原因,并且可以使用最小的代价获得最大的优化。
你可能注意到这篇文章讲了大量的关于组件和元素的内容,但是并没有提到太多关于实例的内容。这是因为,和传统的面向对象的 UI
框架相比较,实例在 React
中并没有那么的重要。
只有当组件声明为类的时候才会拥有实例,但是从来不需要直接创建它的实例,React 会帮你创建和管理它的实例。只有在一些特殊的情况下,组件需要 访问实例 执行操作,比如,为一个表单元素获取焦点,而且这种行为通常是需要避免的。
总结(Summary)
元素是一个简单对象,用于描述你想通过 DOM
节点或者其他组件展示在屏幕上的内容。元素可以在它们的属性中包含其他的元素。创建一个 React
元素的成本非常的低,一个元素一旦被创建,将不能修改。
实例是在组件上使用 this
获得的,它在存储本地状态和执行生命周期的事件非常有用。
函数式组件并不存在实例,类组件存在有实例,但是通常不需要我们管理,有 React
替我们管理这些实例。
创建 React
元素有 React.createElement()
、React.createFactory()
、JSX
、函数式几种方法,不要在代码里用对象来写元素,只需要知道元素的本质是对象即可。
注意 出于安全的考虑,所有的 React
元素都需要声明一个额外的 $$typeof: Symbol.for('react.element')
字段,这个字段在上述的例子中都被忽略了。这边文章主要用于介绍上述概念,所以除非添加该字段或者使用 React.createElement
或 JSX
去修改上面的代码,否则上面的代码并不能正常的运行。
参考
本文主要翻译了 React
的官方文档: React Components, Elements, and Instances,并根据个人理解和习惯稍有调整。