- 教程
- 前端
- 文档
【免费下载链接】Under-the-hood-ReactJS
Entire React code base explanation by visual block schemes (Stack version)
导读
本文基于 Under-the-hood-ReactJS 仓库《Stack Reconciler》系列的第 3 部分,深入剖析 React 15.4.2(Stack 版本)在浏览器中执行组件挂载(Mount)的核心路径——ReactCompositeComponent.mountComponent。文章以可视化流程图(stack/images/3/part-3.svg)为线索,逐一拆解"组件树如何从上到下递归挂载"、"updater为何在挂载时才动态赋值给实例"、"自定义组件实例何时被new出来"、"componentWillMount中的setState为何不会触发重渲染"以及"首个子 VDOM 实例如何被实例化"五个关键问题。读完本文,你将掌握从ReactDOM.render到自定义组件首次被"触碰"再到子元素ReactDOMComponent挂载的完整调用链,能够独立读懂 ReactCompositeComponent.js 中对应的源码片段,并为后续理解setState更新机制(Part 9)打下基础。
回顾上下文:我们正处在挂载链路的哪一环
在进入本文之前,需要明确当前的代码坐标。整个 ReactDOM 挂载流程被拆解为 15 个部分(见 README.md 与 Intro.md),Part 3 之前我们已经完成了:
- 事务(Transaction)机制(Part 1):
ReactDOM.render被包裹在ReactDefaultBatchingStrategyTransaction中,其close阶段会调用ReactUpdates.flushBatchedUpdates,保证挂载完成后统一校验 dirty 组件。 ReactReconcileTransaction(Part 2):通过SELECTION_RESTORATION、EVENT_SUPPRESSION、ON_DOM_READY_QUEUEING三个 wrapper 保持 DOM 状态(如文本选区、事件抑制),随后ReactReconciler.mountComponent作为"平台相关逻辑的中介",把挂载任务委托给真正的组件模块。
而 Part 3 的主角,正是被ReactReconciler委托到的那位:ReactCompositeComponent.mountComponent——文档原话称它是"我们整个旅程中最大的部分之一"。
递归挂载:从 TopLevelWrapper 到第一个"真实"组件
组件树的入口:TopLevelWrapper
需要先纠正一个常见直觉:组件树中被第一个 push 进树的并不是你的应用组件,而是一个 React 内部类TopLevelWrapper。它是空的包装器,不对流程产生任何影响,因此文档建议直接跳过它,从其子组件开始观察。
这也揭示了挂载的本质规律:挂载是一棵树的深度优先递归过程——先挂载父组件,再挂载它的孩子,再挂载孩子的孩子,依次类推。文档明确断言:TopLevelWrapper挂载完成后,它的子组件(即负责管理ExampleApplication的那个ReactCompositeComponent实例)会被立即推入同一阶段继续处理。所以第 (1) 步ReactCompositeComponent.mountComponent的执行对象,就是管理ExampleApplication的内部实例。
ReactCompositeComponent.mountComponent(1) │ ├─ (2) updater = transaction.getUpdateQueue() → 赋给 inst.updater ├─ (3) this._constructComponent() → new ExampleApplication() ├─ (4) 初始挂载:componentWillMount → render └─ (5) this._instantiateReactComponent() → 为 render 返回的元素创建 VDOM 实例为实例动态注入 updater:跨平台的关键设计
updater 是什么
ReactCompositeComponent中第一件关键动作,是把transaction.getUpdateQueue()的返回值(即ReactUpdateQueue模块)赋给组件实例的updater字段(图中步骤 (2))。
为什么要在挂载时动态赋值而不是在类定义中写死?文档给出了清晰解释:
ReactCompositeComponent是所有平台共用的类,而 updater 因平台而异,因此我们根据平台在挂载期间动态地将其赋给实例。
React 需要同时支撑浏览器(ReactDOM)、移动端(ReactNative)、服务端渲染、ReactART 等环境(见 Intro.md)。同一份ReactCompositeComponent无法内置某个平台的更新策略,于是"谁来负责把setState排队、合并、调度"这件事被推迟到挂载阶段,由当前平台的 transaction 提供。这是典型的依赖注入式设计:公共类不关心平台差异,平台差异在运行时被塞进来。
不只是 updater:props / context / refs 一同就位
文档特别强调,这一阶段为实例赋值的并不仅有updater。原文档给出了ReactCompositeComponent.js中对应的源码片段(原文档标注为#255附近):
// src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#255 // 这些应该在构造方法里赋值,但是为了 // 使类的抽象更简单,我们在它之后赋值。 inst.props = publicProps; inst.context = publicContext; inst.refs = emptyObject; inst.updater = updateQueue;也就是说,你的组件实例在挂载阶段被一次性补齐了四个核心成员:
| 字段 | 来源 | 说明 |
|---|---|---|
inst.props | publicProps | 父级传入的属性,之后可通过this.props读取 |
inst.context | publicContext | React context 上下文 |
inst.refs | emptyObject | 初始为空对象的 refs 集合,待子组件挂载后填充 |
inst.updater | updateQueue | 平台相关的更新队列,setState的底层执行者 |
正因如此,你才可以在自己的代码里直接写this.props——这正是上述赋值语句生效的直观结果。
值得牢记:
updater现在虽用不上,但它即将在setState中被使用。Stack 版本中setState的本质是把状态变更enqueue进updater(ReactUpdateQueue.enqueueSetState),再由批处理事务统一 flush。理解这一点,就为阅读 Part 9(setState 更新起点) 埋下了伏笔。
实例化你的组件:new ExampleApplication() 第一次被调用
_constructComponent 的调用链
图中步骤 (3)this._constructComponent是"我们的代码第一次被 React 生态触碰"的里程碑。经过若干构造辅助方法(文档描述为"several construction methods")后,最终执行new ExampleApplication()——此时你写在构造函数里的逻辑才第一次真正运行。
这一点对调试者意义重大:从ReactDOM.render走到这里,此前所有步骤都是 React 内部机制在运转(事务、包装、实例化内部类),直到_constructComponent才首次进入用户代码。文档对此的评价是:"Nice."——这是用户代码与 React 生态的第一次握手。
执行首次挂载:componentWillMount 与 state 的特殊处理
生命周期钩子的出场顺序
步骤 (4) 进入初始挂载(initial mount),第一个发生的行为是调用componentWillMount(仅当组件定义了该方法时)。这是全书遇到的第一个生命周期钩子。
文档同时指出componentDidMount此刻并没有被直接调用,而是被 push 进事务队列(ON_DOM_READY_QUEUEINGwrapper 的职责之一),要等所有挂载操作全部完成、事务close时才真正执行。这也解释了为什么componentDidMount总是晚于所有子组件挂载完毕——它被推迟到了挂载事务收尾阶段。
componentWillMount 里调用 setState:只算 state,不触发 render
原文档引用了官方文档的原话来佐证行为:
componentWillMount()is invoked immediately before mounting occurs. It is called beforerender(), therefore setting state in this method will not trigger a re-rendering.
随后给出了源码级验证(ReactCompositeComponent.js#476附近):
// src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#476 if (inst.componentWillMount) { //.. inst.componentWillMount(); // 当挂载时,在 componentWillMount 中调用的 setState // 会设置 this._pendingStateQueue 而不触发重渲染 if (this._pendingStateQueue) { inst.state = this._processPendingState(inst.props, inst.context); } }这里有两个值得展开的机制细节:
_pendingStateQueue:componentWillMount内调用setState并不会直接改写inst.state,而是把待更新的 state 对象排入this._pendingStateQueue。_processPendingState:挂载分支随后检测到_pendingStateQueue存在,立即调用_processPendingState(inst.props, inst.context)把队列中的多个 partial state 按序合并,一次性重新计算inst.state。
为什么"不调用render也合理"?因为组件此时尚未挂载完成,立刻重渲染既无意义也无处安放结果;React 用"合并 pending state + 延后渲染"的方式,既让开发者能在componentWillMount里安全地预置初始状态,又避免了无谓的重复渲染开销。
state 重算之后:调用你的 render
state重算完成后,流程继续调用组件中开发者书写的render方法——这是"我们的代码"第二次被触碰。render返回的 JSX 描述(在我们的示例中是一个div)将成为下一步创建 VDOM 实例的输入。
创建子元素 VDOM 实例:从元素到 ReactDOMComponent
_instantiateReactComponent 的第二次登场
图中步骤 (5)this._instantiateReactComponent此前已经出现过一次(在 Part 3 之前,它曾为ExampleApplication实例化出ReactCompositeComponent)。这一次它被再次调用,目的不同:
- 上一次:为
ExampleApplication组件元素创建ReactCompositeComponent内部实例; - 这一次:基于
render返回的元素,为它的孩子创建 VDOM 实例。
在我们的具体场景中,render返回的是div,因此其 VDOM 表示是ReactDOMComponent(注意:中文翻译版文档此处写作ReactDOMElement,英文原版为ReactDOMComponent,应以 Part-3.md 原文为准)。
再次进入 ReactReconciler.mountComponent
实例创建完成后,代码再次调用ReactReconciler.mountComponent,但这次传入的internalInstance是新创建的ReactDOMComponent实例,随后继续调用它的mountComponent……如此递归向下,挂载将持续深入直到叶子节点(真实 DOM 元素的创建在 Part 4 中展开)。
这就是整棵组件树被"从上到下、递归深入"地挂载起来的机制:每一层的mountComponent都负责"初始化自身 + 实例化自己的孩子 + 触发下一层挂载"。Part 3 的收尾阶段,控制权由此从"复合组件"(管理自定义组件的层)移交到"宿主组件"(管理真实 DOM 的层)。
流程图回顾:Part 3 的三种抽象层次
原文档在本部分结尾提供了三张递进的示意图(均在 stack/images/3/ 目录下),分别对应三种抽象层次:
- 完整版(part-3.svg,图 3.0):Part 3 全部调用关系的原始流程图,包含上述 (1)~(5) 全部节点;
- 简化版(part-3-A.svg,图 3.1)与整理版(part-3-B.svg,图 3.2):剔除冗余连线、规整排版后的核心流程;
- 精华版(part-3-C.svg,图 3.3):提炼出的"essential value",将直接并入全书最终的
mounting总图(stack/images/6/overall-mounting-scheme.svg 所在的分支)。
图 3.0 Part 3 完整流程图(SVG 可点击放大)
小结:Part 3 的四个核心结论
- 递归挂载模型:挂载 = 父组件挂载 → 子组件挂载 → 孙组件挂载……
TopLevelWrapper仅作入口,不产生实际影响。 - 实例注入时机:
props、context、refs、updater四者均在挂载阶段由ReactCompositeComponent动态赋给自定义实例,其中updater(ReactUpdateQueue)是平台差异的注入点,也是未来setState的执行通道。 - 生命周期首秀:
componentWillMount是第一个被调用的钩子;其内部setState只写入_pendingStateQueue并通过_processPendingState合并重算state,不触发render;componentDidMount被延迟到挂载事务的收尾阶段。 - 实例化与递归交接:
_constructComponent首次执行new ExampleApplication()(用户代码首触),_instantiateReactComponent第二次被调用时为render返回的div创建ReactDOMComponent,再次经ReactReconciler.mountComponent向下递归——真正的 DOM 元素创建留待 Part 4 讲解。
至此,Part 3 的挂载核心链路已经清晰。下一步可继续阅读 Part 4(子元素挂载:props 校验与document.createElement),或返回 栈式协调器章节总览 定位自己的阅读进度。
- 教程
- 前端
- 文档
【免费下载链接】Under-the-hood-ReactJS
Entire React code base explanation by visual block schemes (Stack version)
相关推荐
10个关键点理解React组件挂载过程:Under-the-hood-ReactJS实践指南
10个关键点理解React组件挂载过程:Under the hood ReactJS实践指南 Under the hood ReactJS是一个通过可视化流程图
教程前端文档React组件树遍历终极指南:深入Under-the-hood-ReactJS架构解析
React组件树遍历终极指南:深入Under the hood ReactJS架构解析 Under the hood ReactJS是一个通过可视化流程图详细解
教程前端文档Under the hood ReactJS:用可视化流程图剖析 React Stack reconciler 的挂载与更新机制
Under the hood ReactJS:用可视化流程图剖析 React Stack reconciler 的挂载与更新机制 本文基于开源仓库 Under
教程前端文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考