React组件/元素与实例分析

做者:Dan Abramov
译者:Jogis
译文连接:https://github.com/yesvods/Blog/issues/5
转载请注明译文连接以及译者信息html

前言

不少React新手对Components以及他们的instances和elements之间的区别感到很是困惑,为何要用三种不一样的术语来表明那些被渲染在荧屏上的内容呢?react

亲自管理实例(Managing the Instances)

若是是刚入门React,那么你应该只是接触过一些组件类(component classes)以及实例(instances)。打比方,你可能经过class关键字声明了一个Button组件。这个程序运行时候,可能会有几个Button组件的实例(instances)运行在浏览器上,每个实例会有各自的参数(properties)以及本地状态(state)。这种属于传统的面向对象UI编程。那么为何会有元素(elements)出现呢?git

在这种传统UI模式上,你须要负责建立和删除实例(instances)的子组件实例。若是一个Form的组件想要渲染一个Button子组件,须要实例化这个Button子组件,而且手动更新他们的内容。github

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-native

因而,React又有什么不一样呢?api

用元素来描述节点树(Elements Describe the Tree)

React提出一种元素(elements)来解决这个问题。一个元素仅仅是一个纯的JSON对象,用于描述这个组件的实例或者是DOM节点(译者注:好比div)和组件所须要的参数。元素仅仅包括三个信息:组件类型(例如,Button)、组件参数(例如:color)和一些组件的子元素数组

一个元素(element)实际上并不等于组件的实例,更确切地说,它是一种方式,去告诉React在荧屏上渲染什么,你并不能调用元素的任何方法,它仅仅是一个不可修改的对象,这个对象带有两个字段:type: (string | ReactClass)props: Object1浏览器

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>DOM

  • DangerButton是一个附带特定参数的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,MessageButton都是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对象存在就好。

更多相关内容


  1. 出于安全考虑,全部React元素须要在对象下声明一个额外的 $$typeof:Symbol.for(‘react.element‘) 字段。它在上面的例子被忽略了。这篇博客从头开始用行内对象来表示元素,来告知你底层运做的概念。可是,除非你要么添加$$typeof 到元素上或者用 React.createElement 或JSX去修改上面的代码,不然那些代码并不能正常执行。
相关文章
相关标签/搜索