英文原文 : React Components, Elements, and Instances
组件(Components
)和组件实例(their Instances
),还有元素(elements
)这几个概念困扰了很多React初学者。为什么同是需要渲染到屏幕上的东西却需要三个不同的概念呢 ?
管理实例 Manage the Instances
如果你是一个React新手,很可能之前你只跟组件类(component class)和实例(instance)打过交道。举个例子,你可能会通过创建一个类class
来声明一个按钮组件(Button component
)。应用程序运行的时候,在屏幕上你可能有这个组件(component
)的好多个实例(instance
),每个实例都有自己的属性和仅仅属于自己的状态。这就是传统的面向对象UI编程。那么为什么还要引入元素(element
)的概念呢?
在传统的UI模型中,怎么创建和销毁组件实例完全取决于你。如果一个表单组件(Form component
)想渲染一个按钮组件(Button component
),它需要创建这个按钮的实例,然后在有任何新信息的时候手工保持该按钮实例到最新状态。举例如下 :
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);
}
}
}
这是一段伪代码,但它多多少少已经很接近你编写组合UI时,通过类似Backbone这样的库,用面向对象的方式可以一致地工作的最终代码了。
每个组件实例必须保持对它自己和所有孩子节点的DOM节点的引用(reference),而且还必须在合适的时机创建,更新和销毁它们。组件可能的状态增加时代码行数可能成平方地增长,而且父组件直接访问孩子组件实例,这会导致你在将来想解耦变得很困难。
那么React又有什么不同呢 ?
元素用来描述树 Elements Describe the Tree
在React中,这是元素element
帮助我们的地方。一个元素是一个普通对象,他描述一个组件实例和它所要求的属性,或者一个DOM节点和它所要求的属性。它仅仅包含以下有关信息 :
- 组件类型 (比如,这是一个按钮
Button
) - 属性 (比如,它的颜色
color
) - 它所包含的若干个子元素
一个元素并不是一个的实例。相反,它是一种告诉React你想在屏幕上看到什么的手段。元素上的任何方法你都不能调用。它只是一个不可变的描述性对象,仅有两个字段(field):type(string|ReactClass)
和 props : Object
。
DOM元素 DOM Elements
当一个元素类型type
是字符串(string
)时,该元素表示一个DOM节点,其类型字符串是该DOM节点的标签名称,另外一个属性props
对应地表示DOM节点属性。这是React要往屏幕上渲染的内容。例子如下 :
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
props: {
children: 'OK!'
}
}
}
}
该元素只是以一种普通对象的形式描述了如下HTML片段 :
<button class='button button-blue'>
<b>
OK!
</b>
</button>
注意一下元素是怎么嵌套的。这里的约定是,当我们想创建一棵元素树的时候,我们将容器元素的属性props.children
设置为一个或者多个子元素。
这里比较重要,值得一提的是,父子节点都是描述,而不是真正的实例。当你创建它们时,他们不指向屏幕上的任何东西。你可以创建它们然后把它们丢掉,这都没太大关系。
React元素很容易遍历,不需要分析,而且他们显然比真正的DOM元素更轻量级,因为他们只是普通的对象(object)!
组件元素 Component Elements
然而,元素类型type
也可以是对应到一个React组件的一个函数或者类:
{
type: Button,
props: {
color: 'blue',
children: 'OK!'
}
}
这正是React的核心概念。
描述组件的元素也还是元素,跟描述DOM节点的元素一样。他们也可以跟其他元素嵌套或者混在一起使用。
该功能能够让你定义DangerButton
为一个指定颜色color
的按钮Button
并且彻底地无需担心Button
是被渲染成了一个DOM <button>
,一个DOM <div>
还是一个其他什么东西 :
const DangerButton = ({ children }) => ({
type: Button,
props: {
color: 'red',
children: children
}
});
在同一棵元素树中,你可以混合和匹配使用DOM和组件元素如下 :
const DeleteAccount = () => ({
type: 'div',//DOM元素
props: {
children: [{
type: 'p',//DOM元素
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>
);
这种混合和匹配的使用方式(mix and match)将组件之间解耦开来,因为它们在组合的过程中排他地使用了is-a和has-a关系 :
Button
是一个带有特定属性的 DOM<button>
。DangerButton
是一个带有特定属性的Button
。DeleteAccount
在一个<div>
中包含了一个Button
和一个DangerButton
。
组件封装了元素树 Components Encapsulate Element Trees
当React看到一个元素(element
)的类型(type
)是函数(function
)或者类(class
)时,它知道利用这个组件(component
)的相应属性(props
)得知它需要渲染(render
)到什么元素(element
)。
举例来讲,当它看到这样一个元素:
{
type: Button,
props: {
color: 'blue',
children: 'OK!'
}
}
React 会询问 Button
它需要渲染到什么上去。然后 Button
返回了这样元素 :
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
props: {
children: 'OK!'
}
}
}
}
React会重复这个步骤,直到它弄明白了页面上所有组件(component
)底层的DOM标签元素(DOM tag element
)是什么。
React就像一个小孩儿,先是十万个为什么,然后每个问题打破沙锅问到底,直到你向他解释得他们能够自己指出世界上每一个小东西都是什么。
还记得上面的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组件(component
),属性(props
)是输入,一棵元素树(element tree
)是输出。
返回的元素树既可以包含描述DOM节点的元素,也可以包含描述其他组件的元素。这样你就可以组合页面上相互独立的各部分而无需依赖对他们内部细节的了解。
我们让React创建,更新和销毁实例。我们使用组件返回的元素描述组件而React负责管理组件的实例。
Components Can Be Classes or Functions 组件可以是类或者函数
在上面的代码中,Form
,Message
,和Button
都是React组件。它们可以被写成类似上面的函数,可以是通过React.createClass
来创建的类,也可以写成继承自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 使用工厂方法 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) As an ES6 class descending from React.Component 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节点被创建或者销毁时可以执行一些自己的逻辑。
函数式组件没那么强大但是相对简单,其行为表现就像只带一个render()
方法的类组件。除非你确实需要仅仅类才有的功能,否则我们建议你尽量使用使用函数式组件。
然而,不管是函数还是类,对React来讲,它们根本上都是一种东西:组件。它们将属性props作为输入,返回元素作为输出。
Top-Down Reconciliation 自顶向下的协调
当你这么调用时 :
ReactDOM.render({
type: Form,
props: {
isSubmitted: false,
buttonText: 'OK!'
}
}, document.getElementById('root'));
React会询问 Form
组件在这个属性props
设置下会返回什么样的元素。它会使用类似下面的简化原语逐级细化对你的组件树的理解:
// 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叫做“协调"(reconciliation)的一个过程的一部分,这个协调过程当你调用ReactDOM.render()
或者setState()
的时候会开始。协调过程结束时,React知道了最终DOM树应该是什么样子,然后一个类似于react-domt
或者react-native
渲染器会计算出更新DOM节点所需要的变化的最小集合应用到DOM树上(如果是React Native,则会渲染到平台相关视图)。
这个逐级细化的过程也是React应用易于优化的原因。如果你的组件树变得很大,对于React变得没办法高效地访问,你就可以告诉React,跳过那些相关属性没有发生变化的部分的"细化"而只处理树中有变化的差异部分。
你可能注意到这篇博客讲了一堆关于组件(component
)和元素(element
)的东西,几乎没怎么说实例(instance
)。真正的原因是,跟在其他绝大多数面向对象UI框架中的情况不同,React中实例没有那么重要。
只有以类形式定义的组件才会有实例存在,而且你从来不用直接创建它们的实例:React替你做了这些事情。尽管存在父组件实例访问子组件实例的机制,它们也仅仅用作命令式动作(比如设置某个域field的焦点focus)而且应该尽量被避免。
React处理了每个类组件的实例创建,所以你可以以面向对象的方式编写组件,可以带有方法(method)和本地状态(local state),但是除此之外,在React的编程模型中,实例没有那么重要,它们都是由React自己来管理的。
Summary 总结
一个元素element
是一个普通对象(plain object),描述了对于一个DOM节点或者其他组件component
,你想让它在屏幕上呈现成什么样子。元素element
可以在它的属性props
中包含其他元素(译注:用于形成元素树)。创建一个React元素element
成本很低。元素element
创建之后是不可变的。
一个组件component
可以通过多种方式声明。可以是带有一个render()
方法的类,简单点也可以定义为一个函数。这两种情况下,它都把属性props
作为输入,把返回的一棵元素树作为输出。
一个实例instance
是你在所写的组件类component class
中使用关键字this
所指向的东西(译注:组件实例)。它用来存储本地状态和响应生命周期事件很有用。
函数式组件(Functional component
)根本没有实例instance
。类组件(Class component
)有实例instance
,但是永远也不需要直接创建一个组件的实例,因为React帮你做了这些。
最后,创建元素的话,使用 React.createElement(),JSX 或者一个 元素工厂助手(element factory helper)。Don’t write elements as plain objects in the real code—just know that they are plain objects under the hood.
延伸阅读 Further Reading
有关资料
译注 : 这部分内容不是翻译自原文,只是跟本文主题很相关。