细读 React | 元素、组件、实例

配图源自 Freepik

一、前言

元素是构成 React 应用最小的砖块,它描述了你在页面上想看到的内容。

const element = <h1>Hello World</h1>

与浏览器的 DOM 元素不同,React 元素是创建开销极小的普通对象。React DOM 会负责更新 DOM 来与 React 元素保持一致。

以上 JSX 语法编写的代码,最终会被 Babel (在线 Babel 编译器)转换为:

const element = React.createElement('h1', null, 'Hello World')

React.createElement() 最后会返回一个普通的 JavaScript 对象(该对象就是对 React 元素的描述):

const element = {
  type: 'h1',
  props: {
    children: 'Hello World'
  },
  // ...
}

可以在控制台打印一下 element 元素:

需要注意的是,React 元素描述的是 Virtual DOM 的结构,而非真实 DOM。真实 DOM 由 React DOM 根据 Virtual DOM 负责更新。

二、React 元素

上面提到,React 元素本身上就是一个 JavaScript 对象,而且是不可变对象(Immutable Object)。

由于 React 元素是不可变对象,若对其 props 等属性进行修改操作,是会抛出错误的。

React 元素表示了某个时刻的 UI,而更新 UI 的唯一方式是创建一个全新的元素。React DOM 会根据 Diff 算法只更新它需要更新的部分。

三、React 组件

刚开始接触 React 时,人们很容易将元素组件的概念混淆。元素是 React 应用最小的单元,而组件则是由一个或多个元素构成的。

组件是 React 中很重要的思想。一个复杂庞大的 React 应用,是由许多结构简单、清晰的组件组合而成的。

组件,从概念上类似于 JavaScript 函数,它接受任意的入参(即 props),并返回 React 元素。

在 React 中,组件分为函数组件class 组件。下面声明了两种不同类型的组件:

// 函数组件
function Comp1() {
  return <h1>Functional Component.</h1>
}

// class 组件
class Comp2 extends React.Component {
  // render 是类组件唯一必需实现的方法
  render() {
    return <h1>Class Component.</h1>
  }
}

我们知道 ReactDOM.render(element, container[, callback]) 方法,第一个参数 element 接收的是 React 元素。而我们声明的组件本质上是一个 JavaScript 函数,因此,我们应该要这样使用自定义组件:

ReactDOM.render(<Comp1 />, document.getElementById('root'))

四、React 元素分类

从以上可知,我们遇到的 React 元素,可以是 DOM 标签:

const element = <h1>Hello World</h1>

也可以是用户自定义的组件:

const element = <Comp1 />

因此,我们可以将 React 元素分为两类:DOM 类型元素、组件类型元素。

前者是指使用类似 divh1pspan 等 DOM 标签创建的 React 元素,而后者是指使用 React 组件(如 Class Components、Functional Components)创建的 React 元素。

对于 DOM 标签,React 可以分辨出来,而自定义的 React 组件呢?若不约定规则,那么它无法辨认啊。所以 React 要求以 JSX 语法编写 React 组件时,其组件名称必须以大写字母开头

例如,MyComponent 是“合法”的 React 组件,而 myComponent 是“不合法”的。否则 React 会发出如下警告 ⚠️:

Warning: The tag <myComponent> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.

上面打了双引号,其实在声明一个 React 组件时,是可以以小写字母开头的,但在使用时必须以大写字母开头。即我们可以将 React 组件赋值给一个以大写字母开头的变量,然后正常使用。但这种“非主流”的写法,不被推荐。再者,声明一个构造函数,它的名称应以大写字母开头,这是一种约定俗成的写法。

同样的,DOM 类型元素只能以小写字母的形式,例如 <div />。如果使用 <Div /> 会被 React 认为是自定义组件,但由于我们又没有声明,因此可能会抛出引用错误:ReferenceError: Div is not defined

想了解更多关于此规范的原因,请看深入 JSX

五、React 元素、组件使用误区

一些容易混淆、出错的写法:

const Element = <h1>React Element.</h1>
const rootEl = document.getElementById('root')

// 正确示例 ✅
ReactDOM.render(Element, rootEl)
ReactDOM.render(<Comp1 />, rootEl)

// 错误示例 ❌
ReactDOM.render(<Element />, rootEl) // 1️⃣
ReactDOM.render(Comp1, rootEl) // 2️⃣

错误示例 1️⃣ 会抛出以下警告 ⚠️:

Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: <h1 />. Did you accidentally export a JSX literal instead of a component?

错误示例 2️⃣ 会抛出以下警告 ⚠️:

Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.

我们都知道,ReactDOM.render(element, container[, callback]) 第一个参数接收 React 元素。而 React 元素都是由 React.createElement() 返回的。我们从 React.createElement() 的语法上解释以上错误示例:

React.createElement(
  type,
  [props],
  [...children]
)
  • type 可以是标签名字符串(如 divspanh1 等),也可以是 React 组件类型(class 组件或函数组件),或者是 React Fragment 类型。

  • props 可选,组件属性

  • children 可选,子元素。含有多个子元素时,最终 React Element 的 props.children 会返回一个数组。

原因分析:

  1. ReactDOM.render(Element, rootEl) 相当于 ReactDOM.render(<h1>React Element.</h1>, rootEl)( 这里 Element 只是一个变量)。以小写字母开头的元素代表一个 HTML 内置组件,在编译时 <h1> 会生成响应的字符串 'h1' 传递给 React.createElement,是符合参数要求的。所以没问题。
  2. ReactDOM.render(<Comp1 />, rootEl)以大写字母开头的元素,对应着自定义组件,<Comp1 /> 会被编译为 React.createElement(Comp1)。而且 Comp1 正是 React 组件(本质上就是 JavaScript 函数),也符合参数要求,所以没问题。
  3. 按上面的规则,<Element /> 以大写字母开头,会被认为是 React 组件,但 Element 的类型是 "object",而不是 "function"(class 本质也是函数),不符合 React.createElement 参数要求,因此会被报错或警告。
  4. ReactDOM.render(Comp1, rootEl) 中的 Comp1 是一个 JavaScript 函数,而非 React 元素,因此也会报错。

六、实例

React 组件是一个函数或者类,而实际发挥作用的是 React 组件的实例对象。只有在组件实例化之后,每个组件实例才有自身的 props、state 或对 DOM 节点的引用。

function Child() {
  return <div>Child Component</div>
}

class Parent extends React.Component {
  // 需要注意的是,在组件将要被销毁的时候会触发此生命周期函数
  // 当组件从页面中“移除”,并不意味着组件实例被回收掉了
  // 仅在组件实例不再被任何地方引用,它才会被垃圾回收。
  componentWillUnmount() {
    // ...
  }

  render() {
    return (
      <div>
         <div>Parent Component</div>
         {/* 当父组件触发 render 之后,子组件就会被实例化 */}
         <Child />
      </div>
    )
  }
}

七、节点

很多情况下,我们会使用 PropTypes 来限制组件属性类型。这里我们提一下与本文相关的两种类型:PropTypes.nodePropTypes.element

import PropTypes from 'prop-types'

function MyComponent(props) {
  return (
    <div>
      <div>node: { props.node }</div>
      <div>element: { props.element }</div>
    </div>
  )
}

MyComponent.propTypes = {
  node: PropTypes.node,
  element: PropTypes.element
}
  • PropTypes.element:可以是 nullundefined 或 React 元素。
  • PropTypes.node: 可以是 nullundefined、字符串、React 元素或包含这些类型的数组。

The end.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容