React 中 Components, Elements 和 Instances 的区别
date
May 3, 2018
slug
components-elements-instances
status
Published
tags
React
summary
type
Post
注:本文内容部分翻译自 React Components, Elements, and Instances,部分源自自己的理解。
在使用 React 时,我们常常会遇到 Components、Elements 与 Instances 三个概念,并且这三者在 React 语境中有明确指向的。熟悉这三个概念之后,我们才能明白当我们在写 JSX 时,我们在写什么。
概要
- An element is a plain object describing a component instance or DOM node.
- A DOM element will have
string
type and custom components will have a fuction type.
- Instances are never accessed publicly.
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 编写方式存在两个问题:
- 代码量会随着组件可能存在的状态的数量的平方倍数增长。
- 父组件可以直接访问子组件会使解耦变得很困难。
在 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 有两种形式:
- class component
- 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 时,我们在干什么」这个问题……