React 组件、元素以及实例[译]

注:英文术语首次出现会有其对应中文翻译的名称,后文将只用中文译名表述。
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-ahas-a 两种关系:

  • Button 是一个有指定属性的 DOM button 元素
  • DangerButton 是一个有指定属性的 Button 元素
  • DeleteAccount 在一个 <div> 内包含了 ButtonDangerButton 元素

组件封装元素树

当 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 负责管理实例。

组件可以是类或者函数

在上面的代码中,FormMessageButton 都是 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.createElementJSX 或者 element factory helper,不要在代码中手动把元素写成纯对象的形式,你只要知道它们是纯对象就好了。

原文:React Components, Elements, and instances

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容