前言
本文的React代码版本为18.2.0
可调试的代码仓库为:GitHub - yyyao-hh/react-debug at master-pure
React Element作为React架构的基石,理解其本质和工作原理对于深入掌握React至关重要。并且后面的章节都依托于前面基础知识的铺垫,所以务必仔细阅读本章内容!!!
基本概念
JSX (JavaScript XML)是一种JavaScript的语法扩展,它允许我们在JavaScript中直接书写类似HTML的结构。它为我们提供了一种简洁、直观且高效的方式描述 UI 组件。
官方文档:JSX 简介 -- React
javascript
/* src/index.js */
function APP() {
return <div class="title">Hello, world!</div>;
};
从 JSX 到 React Element
实际上,JSX并不是React直接执行的代码,它会被编译成标准的JavaScript代码。
- 在
React17之前,JSX代码都会被转化为React.createElement函数进行处理。而这也是为什么之前的项目中需要引入React。
javascript
/* createElement: src/react/packages/react/src/ReactElement.js */
import React from "react"
// JSX 代码
<div className="title">Hello World</div>
// 编译为
React.createElement('div', { className: 'title' }, 'Hello World')
- 在
React17之后,React和babel进行了合作,支持了jsx-runtime。所以我们不用手动引入React,而是由编译器自动引入新的jsx函数 。
php
/* jsx: src/react/packages/react/jsx-runtime.js */
// JSX 代码
<div className="title">Hello World</div>
// 编译为
import { jsx as _jsx } from 'react/jsx-runtime'
_jsx('div', { className: 'title', children: 'Hello World' })
新模式配置:
lua
{
"plugins": [["@babel/plugin-transform-react-jsx", { "runtime": "automatic" }]]
}
使用
create-react-app或Vite等现代工具链,通常默认使用新模式
但是无论是上面的哪种转换方式,最终结果都是被转化为一个普通的JavaScript对象,它描述了你希望在屏幕上看到的内容。
React Element 的数据结构
typescript
/* src/react/packages/shared/ReactElementType.js */
export type ReactElement = {|
$$typeof: any,
type: any,
key: any,
ref: any,
props: any,
_owner: any
// 省略 __DEV__ 环境的一些属性
|};
每个属性的含义如下:
| 属性 | 描述 | 示例值 |
|---|---|---|
$$typeof |
唯一标识这是一个React Element,通常是一个Symbol |
Symbol(react.element) |
type |
元素的类型 | - 字符串:表示原生DOM节点('div') - 函数或类:表示自定义组件(MyComp) |
key |
列表项的唯一标识 | 'item-1' |
ref |
用于访问底层DOM节点或组件实例的引用 |
{current: null} |
props |
元素的属性集合(不包括key和ref) |
{className: 'app', children: ...} |
_owner |
创建此元素的组件对应的Fiber节点 |
FiberNode |
需要特别注意的是$$typeof属性,它是一个Symbol类型的值,用于唯一标识React Element。这个设计主要是为了安全考虑------防止XSS攻击,因为JSON中不能包含Symbol类型的值,即使服务器注入了一个恶意的React Element对象,React也不会认它。
React Element 与 Fiber 节点的关系
理解React Element和Fiber节点之间的关系至关重要,它们是React架构中不同但互补的概念。
不同的角色与职责
- React Element :是一个普通的
JavaScript对象,它描述了UI应该是什么样子。它是不可变的,每次更新都会创建新的Element对象。我们可以把它看作是UI的蓝图:它说明了需要渲染什么,但不会实际执行渲染工作。 - Fiber :是
React调和过程(Reconciliation)中的工作单元。它是在渲染过程中创建的,然后组成了Fiber树。与React Element不同,Fiber是可变的,它们包含组件状态、副作用标记、以及指向其他Fiber的指针(形成链表结构)。
渲染过程中的协作
React渲染过程的步骤,会遵循以下过程:
- 编译阶段:
JSX被转译为createElement/jsx函数调用 - 触发更新:初始渲染、状态更新、属性更新等
- 渲染阶段:
-
- 调用函数组件(或类组件的
render方法)获取新的React Element - 将新的
Element树与上一次渲染的Fiber树进行比较,生成新的Fiber树(Diff算法)
- 调用函数组件(或类组件的
- 提交阶段:将更新应用到实际的
DOM
创建过程
要真正理解React Element,我们需要深入createElement/jsx函数的源码,看看它是如何构造这个核心数据结构的。
注意:代码去除了开发环境相关的逻辑,并精简处理
createElement 函数分析
ini
/* src/react/packages/react/src/ReactElement.js */
const RESERVED_PROPS = {
key: true,
ref: true,
};
export function createElement(type, config, children) {
let propName;
const props = {};
let key = '' + config.key;
let ref = config.ref;
// 过滤掉保留属性(key, ref等), 将其他属性存到 props 对象
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
// 处理 children 参数
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// 处理默认 props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 创建并返回 React Element
return ReactElement(...);
}
jsx 函数分析
逻辑与createElement类似,入参方面将key作为单独参数传递。
ini
/* src/react/packages/react/src/jsx/ReactJSXElement.js */
const RESERVED_PROPS = {
key: true,
ref: true,
};
export function jsx(type, config, maybeKey) {
let propName;
const props = {};
let key = config.key;
let ref = config.ref;
// 过滤掉保留属性(key, ref等), 将其他属性存到 props 对象(children 也在 config 中)
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
// 处理默认 props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 创建并返回 React Element
return ReactElement(...);
}
ReactElement 函数
rust
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
};
return element;
};
要深入理解
React Element的创建过程,最好的方法是在源码中设置断点并进行调试。
总结
很多人将React Element等同于虚拟DOM,实际上React Element确实是React虚拟DOM实现的一部分,而另一部分就是Fiber架构。当组件状态变化时,React会创建新的Element树,然后通过Diff算法比较新旧两棵树,找出最小变更,最后批量更新真实DOM。
下一章我们将了解整个React应用渲染的起点:容器的挂载