注:英文术语首次出现会有其对应中文翻译的名称,后文将只用中文译名表述。
React 初学者很容易被 Components(组件)、组件的 instances(实例)和 elements(元素)搞混,为什么要用三个不同的术语描述 UI 呢?
管理实例
如果你是 React 初学者,你很有可能只接触过组件类和实例。例如,你会通过创建一个类来声明一个 Button
组件,在页面中可能会有这个组件的多个实例,每一个实例都有自己的 propertities(属性)和 local state(本地状态),这是传统的面向对象 UI 编程的做法,那为什么要引入元素的概念呢?
传统的 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 代码中,元素的引入就是为了解决上述问题,元素只是一个用来描述一个组件实例及其 DOM 节点所需属性的纯对象。它只包含组件类型(例如 Button
)、相关属性(例如 color
)以及内部的子元素。
元素并不是一个真正的实例,而是一种用来告诉 React 你希望哪些东西显示在页面中的方式。你不能调用元素里的任何方法,因为它一个不可变的描述对象,包含两个属性:type(string | ReactClass)
和 props(Object)
。
当元素的 type
是一个字符串,就代表 DOM 节点中对应的标签名,props
对应标签上的属性,这就是 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 元素更轻量,因为它们只是普通对象。
组件元素
然而,元素的 type
也可以是一个表示 React 组件的函数或者类:
{
type: Button,
props: {
color: 'blue',
children: 'OK!'
}
}
这就是 React 的核心思想了。
一个描述组件的元素也属于元素的范畴,就像一个描述 DOM 节点的元素一样,它们可以互相嵌套混搭使用
上述这个特点允许你定义一个 DangerButton
组件,并指定一个 color
属性,完全不需要担心 Button
是否引用了一个真实的 DOM 标签 <button>
或者 <div>
或者别的标签。
const DangerButton = ({ children }) => ({
type: Button,
props: {
color: 'red',
children: children
}
});
在一棵 DOM 树中,可以混合使用匹配 DOM 节点或者 React 组件的元素。
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>
);
这种混搭模式能保持组件之间相互解耦,因为可以通过组合唯一地表达 is-a
和 has-a
两种关系:
-
Button
是一个有指定属性的 DOMbutton
元素 -
DangerButton
是一个有指定属性的Button
元素 -
DeleteAccount
在一个<div>
内包含了Button
和DangerButton
元素
组件封装元素树
当 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 标签元素为止。
还记得上面那个 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 负责管理实例。
组件可以是类或者函数
在上面的代码中,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
}
}
}
};
}
}
当一个组件被声明为一个类时,它比函数式组件稍微强大一些,因为可以保存一些本地状态以及在生命周期函数内执行自定义逻辑等。
函数式组件没那么强大,但胜在简单,它就像一个只有 render()
方法的组件类。除非你需要那些类才能提供的特性,否则用函数式组件就好了。
自顶向下的调度
当你这样调用:
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-don
或者 react-native
这样的渲染器将在需要更新的时候执行最少的 DOM 操作。
这个逐步提炼的过程正是 React APP 易于优化的原因,如果组件树的某些部分规模太大,React 访问效率不高,那么你可以告诉 React 如果属性没有变化就略过这部分组件树的提炼操作。当属性是不可变的数据时,可以很快判断它是否发生变化。因此,React 和不可变数据是一对天生的好基友,对于优化性能有事半功倍的效果。
你可能发现这篇文章花了很多篇幅介绍组件和元素,但实例却没怎么提起,实际上,实例在 React 中的作用并没有像在大部分面向对象 UI 框架中那么重要。
只有声明为类的组件才有实例,而且你不用直接创建这些实例,React 会帮你搞定。除了一些必要的场景(例如让某个表单域获得焦点),一般情况下应避免触碰组件实例。
总结
元素就是一个用语描述出现在页面中的 DOM 节点或者 React 组件的纯对象。元素可以在自己的属性中包含其它元素。创建一个元素的成本很低,一旦元素被创建之后,就不再发生变化。
React 组件可以用好几种方式声明,可以是一个包含 render()
方法的类,也可以是一个简单的函数,不管怎么样,它都是以属性作为输入,返回元素树作为输出。
当一个组件被注入一些属性值时,属性值来源于它的父级元素,所以人们常说,属性在 React 中是单向流动的:从父级到子元素。
所谓的实例,就是你在组件类中用 this
引用的那个对象,对于保存本地状态以及介入生命周期函数是有用的。
函数式组件没有实例,类组件才有,但你从来不需要手动创建,React 会帮你搞定。
最后,要想创建元素,可以使用 React.createElement
,JSX
或者 element factory helper
,不要在代码中手动把元素写成纯对象的形式,你只要知道它们是纯对象就好了。
网友评论