- 教程
- 前端
- 文档
【免费下载链接】Under-the-hood-ReactJS
Entire React code base explanation by visual block schemes (Stack version)
本篇文章是「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),建议另开标签页放大阅读,与正文对照:
从流程图可以清晰地读出整个挂载路径,我们可以先做一个总览,后续小节逐一展开:
| 流程步骤 | 关键动作 | 作用 |
|---|---|---|
| 01 | this._tag是否命中复杂标签集合(如source、video、select等) | 决定是否需要额外的包装器处理 |
| — | ReactDOMSelect.mountWrapper(...)+trapBubbledEventsLocal入队 | 为复杂标签补充原生行为与事件监听 |
| 02 | assertValidProps(this, props) | 校验内部props的合法性,非法即抛异常 |
| — | validateDOMNesting(this._tag, ...) | 校验子标签 → 父标签的 DOM 嵌套合法性 |
| — | transaction.useCreateElement分支判断 | 选择以createElement方式创建元素 |
| 03 | ownerDocument.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 源码的引用,并非本仓库文件)。从流程图可以看到,命中复杂标签后会有两个动作:
- 调用包装器的
mountWrapper(this, props, ...),把标签的原生行为与 React 的 props 对齐; - 通过
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 节点诞生了。
元素创建之后,还有一串收尾动作在流程图中清晰可见:
ReactDOMComponentTree.precacheNode(this, el):把内部实例this与真实节点el建立关联并缓存。从源码结构看,这个关联是后续事件委托、更新时快速定位节点的基础设施;this._updateDOMProperties(null, props, transaction):初始化 DOM 属性。注意第一个参数是null——因为首次挂载时「上一个 props」还不存在(第 5 部分正是专门讲lastProps/nextProps两轮循环的 diff 更新,这里只是伏笔);lazyTree = DOMLazyTree(el):把新建元素包装成「延迟构建」的 DOM 树结构,从命名和它在流程图中的位置可以推断,这是为了把子内容的创建与实际插入解耦,避免过早触发浏览器布局;this._createInitialChildren(transaction, props, context, lazyTree):为元素创建初始子内容(示例div里的按钮、文本、子组件等会在这一环节继续递归挂载,这也是后续部分的内容);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)
相关推荐
Agentic Awesome Skills 质量标准指南:从技能提交到 Validated 徽章的完整验证流程
Agentic Awesome Skills 质量标准指南:从技能提交到 Validated 徽章的完整验证流程 本篇指南聚焦 Agentic Awesome
教程前端文档SwiftUI性能优化终极指南:构建快速响应应用的8个核心技术
SwiftUI性能优化终极指南:构建快速响应应用的8个核心技术 SwiftUI作为Apple的声明式UI框架,让开发者能够以更少的代码构建跨平台应用。然而,随着
终极React底层揭秘:Under-the-hood-ReactJS图解三大核心机制
终极React底层揭秘:Under the hood ReactJS图解三大核心机制 Under the hood ReactJS是一个通过可视化流程图解全面解
教程前端文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考