Joyeuxman / Joyeuxman.github.io

个人博客
1 stars 0 forks source link

React Components, Elements, and Instances #17

Open Joyeuxman opened 5 years ago

Joyeuxman commented 5 years ago

React Components, Elements, and Instances

组件、它们的实例和元素之间的区别使许多初学者感到困惑。为什么有三种不同的术语来指代在屏幕上绘制的东西?

管理实例

如果您刚接触React不久,您可能只与组件类、实例打过交道。例如,您可以通过创建一个类来声明一个按钮组件。当应用程序运行时,您可能在屏幕上有这个组件的几个实例,每个实例都有它自己的属性和本地状态。这是传统的面向对象的UI编程。为什么引入元素呢? 在这个传统的UI模型中,由您来负责创建和销毁子组件实例。如果表单组件想要呈现一个按钮组件,它需要创建它的实例,并手动保持它与任何新信息同步。

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);
    }
  }
}

这是伪代码,但它或多或少是您编写复合UI代码时所得到的结果,该代码使用像Backbone这样的库以面向对象的方式运行。

每个组件实例都必须保留对其DOM节点和子组件实例的引用,并在时机合适时创建、更新和销毁它们。当遇到有state的组件,代码的行数可能成倍的增长,并且父组件可以直接访问他们的子组件实例,这使得将来很难将它们解耦。

那么,React有什么不同呢?

元素描述了树

在React中,这是元素来拯救的地方。元素是一个描述组件实例或DOM节点及其所需属性的普通对象。它只包含有关组件类型的信息(例如,一个按钮),它的属性(例如,它的颜色),以及其中的任何子元素。 元素不是实例。相反,它是一种告诉React你想在屏幕上看到什么的方法。在元素上不能调用任何方法。它只是一个具有两个字段的不可变描述对象:type:(string |ReactClass)props:Object

DOM Elements

当一个元素的类型是一个字符串时,它表示一个带有该标签名称的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>

注意元素是如何嵌套的。按照约定,当我们想要创建一个元素树时,我们指定一个或多个子元素作为子元素的元素。 重要的是,子元素和父元素都只是描述,而不是实际的实例。当你创建它们时,它们不会指向屏幕上的任何东西。你可以创造它们,把它们扔掉,这无关紧要。 React元素很容易遍历,不需要解析,当然它们比实际的DOM元素要轻得多——它们只是对象!

Component Elements

但是,元素的类型也可以是React component对应的函数或类:

{
  type: Button,
  props: {
    color: 'blue',
    children: 'OK!'
  }
}

这是React的核心思想。 描述组件的元素也是一个元素,就像描述DOM节点的元素一样。它们可以相互嵌套和混合。 这个特性让您可以将“危险按钮”组件定义为具有特定颜色属性值的按钮,而不必担心按钮是否呈现给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>
);

这种混合和匹配有助于使组件彼此分离,因为它们可以通过组合来表达isahasa关系。 按钮是一个带有特定属性的DOM < Button >。 “危险”按钮是一个具有特定属性的按钮。 DeleteAccount<div>中包含一个按钮和一个危险按钮。

组件封装元素树

当React看到一个带有函数或类类型的元素时,它知道要问这个组件渲染什么元素,并给定相应的props。 当它看到这个元素:

{
  type: Button,
  props: {
    color: 'blue',
    children: 'OK!'
  }
}

React会询问按钮渲染的元素内容。该按钮将返回此元素:

{
  type: 'button',
  props: {
    className: 'button button-blue',
    children: {
      type: 'b',
      props: {
        children: 'OK!'
      }
    }
  }
}

React将重复这个过程,直到它知道页面上每个组件的底层DOM标签元素。 React就像一个孩子问“Y”是什么意思,你向他们解释,直到他们明白了世界上的每一件小事。 还记得上面的表单示例吗?应用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节点的元素和描述其他组件的元素。这使您可以在不依赖于其内部DOM结构的情况下组成UI的独立部分。 我们让React创建、更新和销毁实例。我们用从组件返回的元素来描述这些实例,React负责管理这些实例。

组件可以是类或函数。

在上面的代码中,Form、Message、Button都是React Component。它们可以被编写为函数,比如上面的函数,或者作为从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
          }
        }
      }
    };
  }
}

当一个组件被定义为一个类时,它比功能组件更强大一些。当创建或销毁相应的DOM节点时,它可以存储一些本地state并执行自定义逻辑。 功能组件的功能不那么强大,但更简单,它的作用就像一个类组件,只有一个render()方法。除非您在类中只需要这个特性,否则我们鼓励您使用功能组件。 然而,无论是函数还是类,从根本上来说,它们都是React的组件。他们将这些props作为输入,并将元素作为输出返回。

Top-Down Reconciliation 自上而下的和解(解析)

当你调用:

ReactDOM.render({
  type: Form,
  props: {
    isSubmitted: false,
    buttonText: 'OK!'
  }
}, document.getElementById('root'));

考虑到这些props,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!'
      }
    }
  }
}

当您调用ReactDOM.render()setState()时,React会开始解析,这是React过程的一部分。在React解析结束时,它会知道要渲染DOM树,而一个类似于react-domreact-native的渲染器会应用最小的更改集来更新DOM节点。 这个逐渐精炼的过程也是React应用程序易于优化的原因。如果组件树的某些部分变得太大,导致React不能有效地访问,那么您可以告诉React跳过这个“细化”,如果相关的props没有改变的话,就把树的某些部分分散开来。如果它们是不可变的,那么就可以很快地计算出这些道具是否已经改变了,因此,React和 immutability的工作在一起很好,并且可以在最小的工作量下提供极大的优化。 您可能已经注意到,这个博客条目谈论了许多关于组件component和元素element的内容,而不是关于实例instance的很多内容。事实是,与大多数面向对象的UI框架相比,实例的作用要小得多。 只有被声明为类的组件才有实例,而您永远不会直接创建它们:对您React是这样的。当父组件实例访问子组件实例的机制存在时,它们只用于命令式操作(例如将焦点集中在字段上),并且通常应该避免。 React负责为每个类组件创建一个实例,这样您就可以以面向对象的方式编写组件,并使用方法和本地状态state,但除此之外,实例在React的编程模型中并不十分重要,并且是由React本身来管理的。

总结

元素是一个简单的对象,描述您希望在屏幕上显示的DOM节点或其他组件。元素可以包含其他元素。创建一个React元素很便宜。一旦创建了一个元素,它就不会发生突变。 组件可以以几种不同的方式声明。它可以是带有render()方法的类。或者,在简单的情况下,它可以被定义为一个函数。在这两种情况下,它都以支持作为输入,并返回一个元素树作为输出。 当一个组件接收到一些props作为输入时,这是因为某个特定的父组件返回了带有它的类型和这些props的元素。 实例是您在编写的组件类中this的引用。它对于存储本地状态和响应生命周期事件非常有用。 功能组件根本没有实例。类组件有实例,但您永远不需要创建一个组件实例,React会负责这个。 最后,要创建元素,请使用React.createelement()、JSXelement factory helper。不要将元素作为一个对象写入真正的代码中,只需要知道它们都是底层的普通对象。