前言
元素是构成 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 类型元素、组件类型元素。
前者是指使用类似 div
、h1
、p
、span
等 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
可以是标签名字符串(如div
、span
、h1
等),也可以是 React 组件类型(class 组件或函数组件),或者是 React Fragment 类型。props
可选,组件属性children
可选,子元素。含有多个子元素时,最终 React Element 的props.children
会返回一个数组。
原因分析:
ReactDOM.render(Element, rootEl)
相当于ReactDOM.render(<h1>React Element.</h1>, rootEl)
( 这里Element
只是一个变量)。以小写字母开头的元素代表一个 HTML 内置组件,在编译时<h1>
会生成响应的字符串'h1'
传递给React.createElement
,是符合参数要求的。所以没问题。ReactDOM.render(<Comp1 />, rootEl)
中以大写字母开头的元素,对应着自定义组件,<Comp1 />
会被编译为React.createElement(Comp1)
。而且Comp1
正是 React 组件(本质上就是 JavaScript 函数),也符合参数要求,所以没问题。- 按上面的规则,
<Element />
以大写字母开头,会被认为是 React 组件,但Element
的类型是"object"
,而不是"function"
(class 本质也是函数),不符合React.createElement
参数要求,因此会被报错或警告。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.node
和 PropTypes.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:可以是
null
、undefined
或 React 元素。 - PropTypes.node: 可以是
null
、undefined
、字符串、React 元素或包含这些类型的数组。
The end.