Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React组件/元素与实例分析 #5

Open
yesvods opened this issue Dec 20, 2015 · 2 comments
Open

React组件/元素与实例分析 #5

yesvods opened this issue Dec 20, 2015 · 2 comments
Labels

Comments

@yesvods
Copy link
Owner

yesvods commented Dec 20, 2015

React Component,Elements and Instances

作者:Dan Abramov
译者:Jogis
原文日期:2015/12/18
原文链接:http://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html

前言

很多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>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去修改上面的代码,否则那些代码并不能正常执行。

@dwqs
Copy link

dwqs commented Dec 23, 2015

nice

@yesvods yesvods added the React label Dec 24, 2015
@VectorHo
Copy link

good

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants