React 源码专栏之深入理解 JSX 本质
JSX 是一种类似于 XML 的语法,它被设计为扩展 ECMAScript(即 JavaScript)。这种类 XML 语法让开发者可以在 JavaScript 代码中直接编写结构化的 HTML 元素。
由于 JSX 是一种声明式语法,实际用于构建抽象的视图层,本身并没有定义任何语义。这种抽象不会被引擎或浏览器直接执行,需要通过适配器(如 Babel 编译器和 React 渲染器)将其编译为标准的 ECMAScript,以适配各种显示终端。
在 React 生态系统中,
渲染器
指的是将 React 组件渲染到特定环境中的工具或库。最常见的 React 渲染器是 React DOM,它将 React 组件渲染到 Web 浏览器的 DOM 中。此外,还有其他的 React 渲染器,例如 React Native 用于移动应用开发,React Three Fiber 用于 WebGL 场景渲染等。
JSX 将 HTML(JSX)、CSS 和 JS 包含在同一个文件中,与其说是单一责任原则,更不如说是关注点分离。React 组件的关注点是将一小部分信息渲染到屏幕上,而 HTML(JSX)、CSS 和 JS 都是实现这一目标所需要的部分,因此它们属于同一个关注点。
通过这种方式,可以显著提高组件的内聚性和松耦合度。将组件的所有相关代码(包括结构、样式和行为)放在一个文件中,使组件更加自包含和模块化。当需要修改组件时,开发者只需关注一个文件,减少了跨文件查找和修改的复杂度,从而提高了维护效率。
JSX 原理
本质上,JSX 只是 React.createElement(component, props, ...children) 的语法糖。所有使用 JSX 语法书写的节点,都会被编译器转换,最终以 React.createElement(...) 的方式创建对应的 ReactElement 对象。
ReactElement 对象的数据结构如下:
export type Source = {|
fileName: string,
lineNumber: number,
|};
export type ReactElement = {|
$$typeof: any,
type: any,
key: any,
ref: any,
props: any,
// ReactFiber
_owner: any,
// __DEV__
_store: {validated: boolean, ...},
_self: React$Element<any>,
_shadowChildren: any,
_source: Source,
|};
首先,在上面的 Source 类型表示一个对象,包含与源码文件相关的信息。
-
fileName: string:表示源码文件的文件名,类型是字符串。
-
lineNumber: number:表示代码所在的行号,类型是数字。
ReactElement 类型表示一个 React 元素的结构,包含了创建 React 元素所需的所有信息。以下是各个属性的详细解释:
-
$$typeof
: any:一个特殊属性,用于标识这个对象是一个 React 元素。$$ typeof
通常是一个 Symbol 值,表示 React 元素的类型。其中 React 的元素的类型主要包括以下这些节点:
// src/react/packages/shared/ReactSymbols.js export const REACT_ELEMENT_TYPE = Symbol.for("react.element"); export const REACT_PORTAL_TYPE = Symbol.for("react.portal"); export const REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"); export const REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"); export const REACT_PROFILER_TYPE = Symbol.for("react.profiler"); export const REACT_PROVIDER_TYPE = Symbol.for("react.provider"); export const REACT_CONTEXT_TYPE = Symbol.for("react.context"); export const REACT_SERVER_CONTEXT_TYPE = Symbol.for("react.server_context"); export const REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"); export const REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"); export const REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"); export const REACT_MEMO_TYPE = Symbol.for("react.memo"); export const REACT_LAZY_TYPE = Symbol.for("react.lazy"); export const REACT_SCOPE_TYPE = Symbol.for("react.scope"); export const REACT_DEBUG_TRACING_MODE_TYPE = Symbol.for( "react.debug_trace_mode" ); export const REACT_OFFSCREEN_TYPE = Symbol.for("react.offscreen"); export const REACT_LEGACY_HIDDEN_TYPE = Symbol.for("react.legacy_hidden"); export const REACT_CACHE_TYPE = Symbol.for("react.cache"); export const REACT_TRACING_MARKER_TYPE = Symbol.for("react.tracing_marker"); export const REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED = Symbol.for( "react.default_value" );
这些
$$typeof
值是 React 内部实现的关键部分,通过它们,React 可以识别不同类型的 React 元素和组件,并进行相应的处理。 -
type: any
:表示 React 元素的类型,可以是一个字符串(如 'div'、'span'),也可以是一个 React 组件(类组件或函数组件),在 reconciler 阶段, 会根据 type 执行不同的逻辑。 -
key: any
:React 元素的唯一标识符,用于高效地识别哪些元素发生了变化、被添加或被移除。 -
ref: any
:用于获取对 React 元素或组件实例的引用。 -
props: any
:包含传递给 React 元素的属性和子元素。 -
_owner: any
:ReactFiber 记录创建本对象的 Fiber 节点, 还未与 Fiber 树关联之前, 该属性为 null -
_store: {validated: boolean, ...}
:内部属性,用于存储一些调试相关的信息。validated 属性是一个布尔值,表示该元素是否已经过验证。 -
_self: React$Element<any>
:指向当前元素自身的引用,通常用于调试目的,以便追踪元素的创建过程。 -
_shadowChildren: any
:内部属性,表示元素的子节点,通常用于调试和内部管理。 -
_source: Source
:指向 Source 类型,包含创建该元素的源码位置(文件名和行号),用于错误报告和调试。
如下图代码所示:
我们在前面中讲到的属性都能再控制台上一一显示出来:
React 为什么需要 $$typeof
React 使用 $$typeof 和 Symbol.for 来标识和区分不同类型的 React 元素。这些标识符用于确保 React 内部能够正确处理和渲染元素,并防止潜在的安全问题或冲突。
Symbol.for 使用全局符号注册表,这意味着在不同模块或作用域中使用相同的字符串调用 Symbol.for,都会返回相同的符号。
考虑一个恶意用户试图在你的应用中插入伪造的 React 元素。如果 React 仅仅依赖于普通字符串或简单的对象结构来标识 React 元素,攻击者可能会尝试创建类似结构的对象来欺骗 React。
const fakeElement = {
type: "div",
props: {
className: "fake",
children: "Malicious content",
},
key: null,
ref: null,
};
在上面的这些代码中,如果没有 $$typeof
和 Symbol.for
的保护,React 可能会错误地处理这个伪造的元素,使用 $$typeof 和 Symbol.for 后,攻击者难以伪造有效的 React 元素,因为他们无法生成相同的全局唯一符号。
// src/react/packages/shared/isValidElementType.js
export default function isValidElementType(type: mixed) {
if (typeof type === "string" || typeof type === "function") {
return true;
}
// Note: typeof might be other than 'symbol' or 'number' (e.g. if it's a polyfill).
if (
type === REACT_FRAGMENT_TYPE ||
type === REACT_PROFILER_TYPE ||
(enableDebugTracing && type === REACT_DEBUG_TRACING_MODE_TYPE) ||
type === REACT_STRICT_MODE_TYPE ||
type === REACT_SUSPENSE_TYPE ||
type === REACT_SUSPENSE_LIST_TYPE ||
(enableLegacyHidden && type === REACT_LEGACY_HIDDEN_TYPE) ||
type === REACT_OFFSCREEN_TYPE ||
(enableScopeAPI && type === REACT_SCOPE_TYPE) ||
(enableCacheElement && type === REACT_CACHE_TYPE) ||
(enableTransitionTracing && type === REACT_TRACING_MARKER_TYPE)
) {
return true;
}
if (typeof type === "object" && type !== null) {
if (
type.$$typeof === REACT_LAZY_TYPE ||
type.$$typeof === REACT_MEMO_TYPE ||
type.$$typeof === REACT_PROVIDER_TYPE ||
type.$$typeof === REACT_CONTEXT_TYPE ||
type.$$typeof === REACT_FORWARD_REF_TYPE ||
// This needs to include all possible module reference object
// types supported by any Flight configuration anywhere since
// we don't know which Flight build this will end up being used
// with.
type.$$typeof === REACT_MODULE_REFERENCE ||
type.getModuleId !== undefined
) {
return true;
}
}
return false;
}
isValidElementType 函数通过检查 type 的类型和特定的 $$typeof 属性,来验证传入的类型是否是一个有效的 React 元素类型。这有助于 React 内部处理和渲染元素,确保组件树的合法性和安全性。
通过这个函数来检测,如果我们用 Symbol 标记每个 React 元素,因为服务端的数据不会有 Symbol.for('react.element'),React 就可以检测 element.$$typeof,如果元素丢失或者无效,则可以拒绝处理该元素,这样就保证了安全性。
总结
每个 DOM 元素的结构都可以用 JavaScript 的对象来表示。你会发现一个 DOM 元素包含的信息其实只有三个:
-
标签名 tagName
-
属性 props
-
子元素 children
<div class="box" id="content">
<div class="title">Hello</div>
<button>Click</button>
</div>
上述的 HTML 所有信息可以用合法的 JavaScript 对象来表示:
{
"tag": "div",
"attrs": { "className": "box", "id": "content" },
"children": [
{
"tag": "div",
"attrs": { "className": "title" },
"children": ["Hello"]
},
{
"tag": "button",
"attrs": null,
"children": ["Click"]
}
]
}
如果我们可以使用 JavaScript 对象来描述所有能用 HTML 表示的 UI 信息,但如果描述整个页面内容,JavaScript 代码会变得非常冗长。
于是 React 就把 JavaScript 的语法扩展了一下,让 JavaScript 语言能够支持这种直接在 JavaScript 代码里面编写类似 HTML 标签结构的语法,这样写起来就方便很多了。编译的过程会把类 HTML 的 JSX 结构转换成 JavaScript 的对象结构。
React.createElement 会构建一个 JavaScript 对象来描述你 HTML 结构的信息,包括标签名、属性、还有子元素等。
在这个流程图中,React 构造是指 React 库中的 React.createElement
方法。这个方法负责将 JSX 转换为 JavaScript 对象结构。
完整流程如下:
-
JSX:我们编写的 react 代码。
-
Babel 编译 + React 构造:Babel 会将 JSX 代码编译成 React.createElement 调用。React.js 通过这些调用来构造 JavaScript 对象。
// JSX
const element = <div className="box">Hello World</div>;
// Babel 编译 + React 构造
const element = React.createElement("div", { className: "box" }, "Hello World");
-
JavaScript 对象结构:这一步中,JSX 被转换成了 JavaScript 对象,这些对象描述了 UI 的结构。
-
ReactDOM.render:这个方法将 JavaScript 对象转换为实际的 DOM 元素,并插入到页面中。
ReactDOM.render(element, document.getElementById("root"));
最终完成了一整个项目的渲染。
转载自:https://juejin.cn/post/7394279685969526819