作者:Dan Abramov
译者:Jogis
译文链接:https://github.com/yesvods/Blog/issues/5
转载请注明译文链接以及译者信息
前言
很多React新手对Components以及他们的instances和elements之间的区别感到非常困惑,为什么要用三种不同的术语来代表那些被渲染在荧屏上的内容呢?
亲自管理实例(Managing the Instances)
如果是刚入门React,那么你应该只是接触过一些组件类(component classes)以及实例(instances)。打比方,你可能通过class
关键字声明了一个Button
组件。这个程序运行时候,可能会有几个Button
组件的实例(instances)运行在浏览器上,每一个实例会有各自的参数(properties)以及本地状态(state)。这种属于传统的面向对象UI编程。那么为什么会有元素(elements)出现呢?
在这种传统UI模式上,你需要负责创建和删除实例(instances)的子组件实例。如果一个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节点引用和子组件的实例,并且需要在合适时机去创建、更新、删除那些子组件实例。代码行数会随着组件的状态(state)数量,以平方几何级别增长。而且这样,组件需要直接访问它的子组件实例,使得这个组件以后非常难解耦。
于是,React又有什么不同呢?
用元素来描述节点树(Elements Describe the Tree)
React提出一种元素(elements)来解决这个问题。一个元素仅仅是一个纯的JSON对象,用于描述这个组件的实例或者是DOM节点(译者注:比如div)和组件所需要的参数。元素仅仅包括三个信息:组件类型(例如,Button
)、组件参数(例如:color
)和一些组件的子元素
一个元素(element)实际上并不等于组件的实例,更确切地说,它是一种方式,去告诉React在荧屏上渲染什么,你并不能调用元素的任何方法,它仅仅是一个不可修改的对象,这个对象带有两个字段:type: (string | ReactClass)
和props: Object
1
DOM元素(DOM Element)
当一个元素的type
是一个字符串,代表是一个type
(译者注:比如div)类型的DOM,props
对应的是这个DOM的属性。React就是根据这个规则来渲染,比如:
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
children: 'OK!'
}
}
}
这个元素只是用一个纯的JSON对象,去代表下面的HTML:
<button class='button button-blue'>
<b>
OK!
</b>
</button>
需要注意的是,元素之间是怎么嵌套的。按照惯例,当我们想去创建一棵元素树(译者注:对,有点拗口),我们会定义一个或者多个子元素作为一个大的元素(容器元素)的children
参数。
最重要的是,父子元素都只是一种描述符,并不是实际的实例(instances)。在他们被创建的时候,他们不会去引用任何被渲染在荧屏上的内容。你可以创建他们,然后把他们删掉,这并不会对荧屏渲染产生任何影响。
React元素是非常容易遍历的,不需要去解析,理所当然的是,他们比真实的DOM元素轻量很多————因为他们只是纯JSON对象。
组件元素(Component Elements)
然而,元素的type
属性可能会是一个函数或者是一个类,代表这是一个React组件:
{
type: Button,
props: {
color: 'blue',
children: 'OK!'
}
}
这就是React的核心灵感!
一个描述另外一个组件的元素,依旧是一个元素,就像刚刚描述DON节点的元素那样。他们可以被嵌套(nexted)和相互混合(mixed)。
这种特性可以让你定义一个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>
);
这种混合配对有利于保持组件的相互解耦关系,因为他们可以通过组合(componsition)独立地表达is-a()
和has-a()
的关系:
Button
是一个附带特定参数的<button>
DOMDangerButton
是一个附带特定参数的Button
DeleteAccount
在一个<div>
DOM里包含一个Button
和一个DangerButton
组件封装元素树(Components Encapsulate Element Trees)
当React看到一个带有type
属性的元素,而且这个type
是个函数或者类,React就会去把相应的props
给予元素,并且去获取元素返回的子元素。
当React看到这种元素:
{
type: Button,
props: {
color: 'blue',
children: 'OK!'
}
}
就会去获取Button
要渲染的子元素,Button
就会返回下面的元素:
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
children: 'OK!'
}
}
}
React会不断重复这个过程,直到它获取这个页面所有组件潜在的DOM标签元素。
React就像一个孩子,会去问“什么是 Y”,然后你会回答“X 是 Y”。孩子重复这个过程直到他们弄清楚这个世界的每一个小的细节。
还记得上面提到的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的独立部分,而不需要依赖他们内部的DOM结构。
React会替我们创建更新和删除实例,我们只需要通过组件返回的元素来描述这些示例,React会替我们管理好这些实例的操作。
组件可能是类或者函数(Components Can Be Classes or Functions)
在上面提到的例子里,Form
,Message
和Button
都是React组件。他们都可以被写成函数形式,就像上面提到的,或者是写成继承React.Component
的类的形式。这三种声明组件的方法结果几乎都是相同的:
// 1) As a function of props
// 1) 作为一个接收props参数的函数
const Button = ({ children, color }) => ({
type: 'button',
props: {
className: 'button button-' + color,
children: {
type: 'b',
props: {
children: children
}
}
}
});
// 2) Using the React.createClass() factory
// 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) As an ES6 class descending from React.Component
// 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
}
}
}
};
}
}
当组件被定义为类,它会比起函数方法的定义强大一些。它可以存储一些本地状态(state)以及在相应DOM节点创建或者删除时候去执行一些自定义逻辑。
一个函数组件会没那么强大,但是会更简洁,而且可以通过一个render()
就能表现得就像一个类组件一样。除非你需要一些只能用类才能提供的特性,否则我们鼓励你去使用函数组件来替代类组件。
然而,不管是函数组件或者类组件,基本来说,他们都属于React组件。他们都会以props作为输入内容,以元素作为输出内容
自顶向下的协调(Top-Down Reconciliation)
当你调用:
ReactDOM.render({
type: Form,
props: {
isSubmitted: false,
buttonText: 'OK!'
}
}, document.getElementById('root'));
React会提供那些props
去问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称作协调(reconciliation),在你调用ReactDOM.render()
或者setState()
的时候会被执行。在协调过程结束之前,React掌握DOM树的结果,再这之后,比如react-dom
或者react-native
的渲染器会应用最小必要变更集合来更新DOM节点(或者是React Native的特定平台视图)。
这个逐步精炼的过程也说明了为什么React应用如此容易优化。如果你的组件树一部分变得太庞大以至于React难以去高效访问,在相关参数没有变化的情况下,你可以告诉React去跳过这一步“精炼”以及跳过diff树的其中一部分。如果参数是不可修改的,计算出他们是否有变化会变得相当快。所以React和immutability结合起来会非常好,而且可以用最小的代价去获得最大的优化。
你可能发现这篇博客一开始谈论到很多关于组件和元素的内容,但是并没有太多关于实例的。事实上,比起大多数的面向对象UI框架,实例在React上显得并没有那么重要。
只有类组件可以拥有实例,而且你从来不需要直接创建他们:React会帮你做好。当存在父组件实例访问子组件实例的情况下,他们只是被用来做一些必要的动作(比如在一个表单域设置焦点),而且通常应该要避免这样做。
React为每一个类组件维护实例的创建,所以你可以以面向对象的方式,用方法和本地状态去编写组件,但是除此之外,实例在React的变成模型上并不是很重要,而且会被React自己管理好。
总结
元素是一个纯的JSON对象,用于描述你想通过DOM节点或者其他组件在荧屏上展示的内容。元素可以在他们的参数里面包含其他元素。创建一个React元素代价非常小。一个元素一旦被创建,将不可更改。
一个组件可以用几种不同的方式去声明。可以是一个带有render()
方法的类。作为另外一种选择,在简单的情况下,组件可以被定义为一个函数。在两种方式下,组件都是被传入的参数作为输入内容,以返回的元素作为输出内容。
如果有一个组件被调用,传入了一些参数作为输入,那是因为有一某个父组件返回了一个带有这个组件的type
以及这些参数(到React上)。这就是为什么大家都认为参数流动方式只有一种:从父组件到子组件。
实例就是你在组件上调用this时候得到的东西,它对本地状态存储以及对响应生命周期事件非常有用。
函数组件根本没有实例,类组件拥有实例,但是你从来都不需要去直接创建一个组件实例——React会帮你管理好它。
最后,想要创建元素,使用React.createElement()
,JSX或者一个元素工厂工具。不要在实际代码上把元素写成纯JSON对象——仅需要知道他们在React机制下面以纯JSON对象存在就好。