likes
comments
collection
share

React 源码专栏之深入理解 JSX 本质

作者站长头像
站长
· 阅读数 28

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 类型表示一个对象,包含与源码文件相关的信息。

  1. fileName: string:表示源码文件的文件名,类型是字符串。

  2. lineNumber: number:表示代码所在的行号,类型是数字。

ReactElement 类型表示一个 React 元素的结构,包含了创建 React 元素所需的所有信息。以下是各个属性的详细解释:

  1. $$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 元素和组件,并进行相应的处理。

  2. type: any:表示 React 元素的类型,可以是一个字符串(如 'div'、'span'),也可以是一个 React 组件(类组件或函数组件),在 reconciler 阶段, 会根据 type 执行不同的逻辑。

  3. key: any:React 元素的唯一标识符,用于高效地识别哪些元素发生了变化、被添加或被移除。

  4. ref: any:用于获取对 React 元素或组件实例的引用。

  5. props: any:包含传递给 React 元素的属性和子元素。

  6. _owner: any:ReactFiber 记录创建本对象的 Fiber 节点, 还未与 Fiber 树关联之前, 该属性为 null

  7. _store: {validated: boolean, ...}:内部属性,用于存储一些调试相关的信息。validated 属性是一个布尔值,表示该元素是否已经过验证。

  8. _self: React$Element<any>:指向当前元素自身的引用,通常用于调试目的,以便追踪元素的创建过程。

  9. _shadowChildren: any:内部属性,表示元素的子节点,通常用于调试和内部管理。

  10. _source: Source:指向 Source 类型,包含创建该元素的源码位置(文件名和行号),用于错误报告和调试。

如下图代码所示:

React 源码专栏之深入理解 JSX 本质

我们在前面中讲到的属性都能再控制台上一一显示出来:

React 源码专栏之深入理解 JSX 本质

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,
};

在上面的这些代码中,如果没有 $$typeofSymbol.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 源码专栏之深入理解 JSX 本质

在这个流程图中,React 构造是指 React 库中的 React.createElement 方法。这个方法负责将 JSX 转换为 JavaScript 对象结构。

完整流程如下:

  1. JSX:我们编写的 react 代码。

  2. 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");
  1. JavaScript 对象结构:这一步中,JSX 被转换成了 JavaScript 对象,这些对象描述了 UI 的结构。

  2. ReactDOM.render:这个方法将 JavaScript 对象转换为实际的 DOM 元素,并插入到页面中。

ReactDOM.render(element, document.getElementById("root"));

最终完成了一整个项目的渲染。

转载自:https://juejin.cn/post/7394279685969526819
评论
请登录