news 2026/10/7 2:28:47

ReactJS 底层揭秘(第 4 部分):ReactDOMComponent 挂载——复杂标签包装、Props 校验与真实 DOM 元素创建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ReactJS 底层揭秘(第 4 部分):ReactDOMComponent 挂载——复杂标签包装、Props 校验与真实 DOM 元素创建
  • 教程
  • 前端
  • 文档

【免费下载链接】Under-the-hood-ReactJS

Entire React code base explanation by visual block schemes (Stack version)

项目地址:https://gitcode.com/gh_mirrors/un/Under-the-hood-ReactJS
点击查看免费下载

本篇文章是「Under-the-hood-ReactJS」系列中文版第 4 部分的技术解析。在第 3 部分中我们走完了自定义组件的挂载(ReactCompositeComponent.mountComponent),本篇将镜头转向其子节点——ReactDOMComponent.mountComponent,沿着流程图拆解「复杂标签包装 → Props 校验 → 创建真实 HTML 元素」这三个关键环节,回答一个核心问题:React 的虚拟 DOM 到底在哪一刻变成浏览器里那个可被看见的真实<div>?读完后你将掌握 Stack 版协调器中 DOM 组件首次挂载的完整调用路径、内部校验规则,以及这些设计在源码中的落点。

本篇在整体挂载流程中的位置

回顾前文:在 第 3 部分 中,ExampleApplication组件通过_instantiateReactComponent被实例化为ReactCompositeComponent,其render()方法返回了<div>,于是基于该元素创建了ReactDOMComponent实例,并再次通过ReactReconciler.mountComponent进入挂载阶段。第 4 部分正是从ReactDOMComponent.mountComponent(transaction, ...)这个入口开始的,它负责把一个代表 HTML 标签的虚拟 DOM 实例真正挂载到浏览器中。

下面这张就是第 4 部分的完整流程图(原书图 4.0),建议另开标签页放大阅读,与正文对照:

从流程图可以清晰地读出整个挂载路径,我们可以先做一个总览,后续小节逐一展开:

流程步骤关键动作作用
01this._tag是否命中复杂标签集合(如source、video、select等)决定是否需要额外的包装器处理
—ReactDOMSelect.mountWrapper(...)+trapBubbledEventsLocal入队为复杂标签补充原生行为与事件监听
02assertValidProps(this, props)校验内部props的合法性,非法即抛异常
—validateDOMNesting(this._tag, ...)校验子标签 → 父标签的 DOM 嵌套合法性
—transaction.useCreateElement分支判断选择以createElement方式创建元素
03ownerDocument.createElement(this._currentElement.type)创建真实 HTML 元素(虚拟 DOM 第一次落地为真实 DOM)
—ReactDOMComponentTree.precacheNode(this, el)将内部实例与真实节点建立关联缓存
—this._updateDOMProperties(null, props, transaction)初始化 DOM 属性(首次挂载时lastProps为null,细节见第 5 部分)
—lazyTree = DOMLazyTree(el)包装元素为「延迟构建的 DOM 树」
—this._createInitialChildren(transaction, props, context, lazyTree)创建初始子内容
—mountImage = lazyTree >>将挂载产物(mount image)返回给上层

子元素挂载:复杂标签的包装器

流程图的第一个判断(步骤 01)检查this._tag是否属于一个「复杂标签」集合,包括source、video、select等(原文档中还列举了form、textarea)。为什么要单独区分它们?因为这类标签无法通过普通的属性/子节点处理一步到位:

  • 媒体类标签(如audio、video):需要对每个媒体事件补充更多事件监听器,例如给audio标签增加volumechange事件监听;
  • 表单类标签(如select、textarea):需要封装一些浏览器原生行为,例如select的选项与选中状态、textarea的默认值等。

React 为这类元素准备了一批专用的包装器(wrappers),原书引用的源码位置是 React v15.4.2 的src\renderers\dom\client\wrappers\目录,典型代表就是ReactDOMSelect与ReactDOMTextarea(此路径是原书对 React 源码的引用,并非本仓库文件)。从流程图可以看到,命中复杂标签后会有两个动作:

  1. 调用包装器的mountWrapper(this, props, ...),把标签的原生行为与 React 的 props 对齐;
  2. 通过transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, ...)把冒泡事件捕获(trap)推迟入队——事件监听要等 DOM 真正就绪后才绑定,这正是事务(transaction)机制在这里的价值(事务的包装器机制可回顾 第 2 部分 中的ReactReconcileTransaction)。

而在我们的示例场景中,render()返回的只是一个简单的<div>——既不涉及媒体事件,也不需要表单原生行为,因此直接跳过包装器,进入下一步的 Props 校验,不需要任何额外处理。

Props 验证:assertValidProps

第二个关键动作(步骤 02)是调用assertValidProps(this, props)。它的职责很明确:确保内部props被设置正确,否则就抛出异常,把开发者写错的用法在挂载早期就拦截下来,而不是等到浏览器报出晦涩的 DOM 错误。

原文档给出了一个最典型的例子——dangerouslySetInnerHTML。它通常在我们需要基于一个字符串直接插入 HTML时使用,其正确形态必须是一个包含__html键的对象:

// 正确的用法 <div dangerouslySetInnerHTML={{ __html: '<span>hello</span>' }} /> // 错误:对象中缺少 __html 键 <div dangerouslySetInnerHTML={{ html: '<span>hello</span>' }} />

一旦你设置了props.dangerouslySetInnerHTML,却忘记提供__html键,React 会抛出如下异常(原文档原文引用):

props.dangerouslySetInnerHTMLmust be in the form{__html: ...}. Please visit https://fb.me/react-invariant-dangerously-set-inner-html for more information.

(props.dangerouslySetInnerHTML必须符合{__html: ...}的形式)

这正是assertValidProps校验逻辑的一部分——「例如」二字也暗示该校验不止覆盖这一项,其整体目标是在进入真正的 DOM 操作之前,保证所有内部 props 的形态都是合法、可安全落地的。

紧接着,流程图中还有一步validateDOMNesting(this._tag, ...)的嵌套校验:它验证子标签 → 父标签的层级是否合法(例如<select>下只允许option、optgroup或文本节点,规则源自 HTML 解析规范)。如果你见过<div> cannot appear as a descendant of <p>这类报错,就是它在工作。这一机制在 第 0 部分 中作为「有趣的事实」被专门介绍过,第 4 部分把它正式纳入了 DOM 组件的挂载路径。

创建 HTML 元素:虚拟 DOM 第一次变成真实 DOM

通过上述校验后,流程图进入步骤 03——创建 HTML 元素。这里有一个前置分支判断transaction.useCreateElement:当事务配置允许使用createElement方式(现代浏览器路径)时,执行:

el = ownerDocument.createElement(this._currentElement.type);

ownerDocument.createElement会实例化出真实的 HTMLdiv。这是整个系列到目前为止的「第一次见面」:此前的所有工作——JSX 转元素、实例化ReactCompositeComponent、调用render()、实例化ReactDOMComponent——处理的都只是虚拟的表现形式(虚拟 DOM),而现在,一个真正可以放进页面、可以被用户看见的 DOM 节点诞生了。

元素创建之后,还有一串收尾动作在流程图中清晰可见:

  1. ReactDOMComponentTree.precacheNode(this, el):把内部实例this与真实节点el建立关联并缓存。从源码结构看,这个关联是后续事件委托、更新时快速定位节点的基础设施;
  2. this._updateDOMProperties(null, props, transaction):初始化 DOM 属性。注意第一个参数是null——因为首次挂载时「上一个 props」还不存在(第 5 部分正是专门讲lastProps/nextProps两轮循环的 diff 更新,这里只是伏笔);
  3. lazyTree = DOMLazyTree(el):把新建元素包装成「延迟构建」的 DOM 树结构,从命名和它在流程图中的位置可以推断,这是为了把子内容的创建与实际插入解耦,避免过早触发浏览器布局;
  4. this._createInitialChildren(transaction, props, context, lazyTree):为元素创建初始子内容(示例div里的按钮、文本、子组件等会在这一环节继续递归挂载,这也是后续部分的内容);
  5. mountImage = lazyTree >>:挂载产物(mount image)作为返回值逐层向上传递,最终由整个挂载流程的收尾阶段将其插入到容器中(这一「最终插入」发生在整体挂载事务的尾部)。

回顾与提炼:第 4 部分的本质

现在让我们像原书一样把流程图「瘦身」。先去掉冗余的次要分支,得到简化版:

再调整间距与对齐:

最终,第 4 部分的本质可以浓缩为下面这张图——判断标签复杂度 → 校验 props 与嵌套合法性 → 创建真实 DOM 元素 → 建立实例关联并初始化:

至此,我们完成了对ReactDOMComponent.mountComponent的完整拆解。下一部分(第 5 部分)将紧接着展开_updateDOMProperties里两轮 props 循环的 diff 更新细节——那里是 React 挂载性能优化的关键一环。

对照阅读与延伸

如果你想继续深入,本仓库中与本篇强相关的资源如下:

  • 本篇中文原文档:stack/languages/chinese/book/Part-4.md
  • 英文原版:stack/book/Part-4.md
  • 上文衔接:第 3 部分 · 挂载,下文:第 5 部分 · 更新 DOM 属性
  • 事务与包装器机制背景:第 2 部分;validateDOMNesting首次登场:第 0 部分
  • 全套流程图:stack/images/4/(part-4.svg为完整图,part-4-A/B/C.svg为逐级精简图)
  • 系列导读与示例代码(ExampleApplication组件):stack/languages/chinese/book/Intro.md

上一节:第 3 部分 | 下一节:第 5 部分 | 返回中文版主页

  • 教程
  • 前端
  • 文档

【免费下载链接】Under-the-hood-ReactJS

Entire React code base explanation by visual block schemes (Stack version)

项目地址:https://gitcode.com/gh_mirrors/un/Under-the-hood-ReactJS
点击查看免费下载
上一篇:终极指南:如何快速解决Steam Deck在Windows上的控制器映射问题
下一篇:如何像专业安全研究员一样高效使用FOFA Viewer:从零到精通的实战指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 2:28:23

Kubeless 自动化发布流程实战解析:从 Git Tag 到多平台 Release 资产

后端云原生微服务 【免费下载链接】kubeless Kubernetes Native Serverless Framework 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ku/kubeless 点击查看 免费下载 Kubeless 是一个 Kubernetes 原生的 Serverless 框架&#xff0c;其版本发布完全由 CI 流水线驱动&…

作者头像 李华