React 的源码与原理解读(三):从 Render 开始,理解元素的挂载和解析
创始人
2025-05-31 19:52:43
0

写在专栏开头(叠甲)

  1. 作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。

  2. 本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。

  3. 本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。

本一节的内容

本节的我们将从 React 的入口函数出发,谈谈在 render() 之后,React 做了什么事情,这部分会涉及到我们怎么样生成一个 Fiber 根节点以及一棵 Fiber 树,然后将它 挂载到真实 DOM 上 的问题,所以需要您先阅读前两章的内容来了解什么是 Fiber

Render 的调用

众所周知,在 React 18.0 之前,我们的根节点是这样挂载的:我们在页面获取一个元素,然后把我们的应用挂载到它内部:

import ReactDOM from 'react-dom';
import App from './App';const root = document.getElementById('root');
ReactDOM.render(, root);

而 18.0 版本之后,我们的 render 是这样调用的 :我们调用了一个 createRoot 方法去创建一个根元素,之后将我们的应用挂载到它的内部

import ReactDOM from 'react-dom/client';
import App from './App';const root = ReactDOM.createRoot(document.getElementById('root'));
root.render();

createRoot

根据上面的逻辑,我们调用的 createRoot 方法创建了一个根元素,我们可以来看看 createRoot 做了什么

你可以直接阅读源码,它在 React 源码的这个位置 :packages/react-dom/src/client/ReactDOMRoot.js

export function createRoot(container: Element | Document | DocumentFragment,options?: CreateRootOptions,
): RootType {// 判断传入元素是元素是否合法可以挂载的元素if (!isValidContainer(container)) {throw new Error('createRoot(...): Target container is not a DOM element.');}// 如果传入body或已使用的元素,发出警告(开发环境)warnIfReactDOMContainerInDEV(container);//这部分和可选参数 options 相关,可以查看React 的相关配置,这里就不具体说明了let isStrictMode = false;    // 严格模式let concurrentUpdatesByDefaultOverride = false;    // 设置更新模式let identifierPrefix = '';    //  前缀let onRecoverableError = defaultOnRecoverableError;  // 可恢复的错误处理方法let transitionCallbacks = null;    // 过度回调if(options !== null && options !== undefined) {/** 设置严格模式 */if(options.unstable_strictMode === true) {isStrictMode = true;}/** 设置 ConcurrentUpdatesByDefaultMode 为 true  */ if(allowConcurrentByDefault &&options.unstable_concurrentUpdatesByDefault === true) {concurrentUpdatesByDefaultOverride = true;}/** 设置前缀 */if(options.identifierPrefix !== undefined) {identifierPrefix = options..identifierPrefix;}/** 设置可恢复的错误处理回调 */if(options.onRecoverableError !== undefined) {onRecoverableError = options.onRecoverableError;}/** 设置过渡回调 */if(options.unstable_transitionCallbacks !== undefined) {transitionCallbacks = options.unstable_transitionCallbacks;}}/*** 这里省略了和配置项以及 DEV 模式相关的代码  ***/// 创建一个 FiberRootNode 类型的节点,fiberRootNode 是整个应用的根节点const root = createContainer(container,ConcurrentRoot,  //1null,isStrictMode,concurrentUpdatesByDefaultOverride,identifierPrefix,onRecoverableError,transitionCallbacks,);// 将 root 挂载到 DOM 节点上markContainerAsRoot(root.current, container);// 获取传入元素的的真实 DOMconst rootContainerElement: Document | Element | DocumentFragment =container.nodeType === COMMENT_NODE? (container.parentNode: any): container;// 绑定所有可支持的事件listenToAllSupportedEvents(rootContainerElement);// 使用 ReactDOMRoot 初始化一个对象return new ReactDOMRoot(root);
}

在这个函数中:

  • 我们首先判定提供的用于挂载在的元素是不是一个合法的 DOM 元素,或者是否已经使用过
  • 之后我们创建一个 FiberRootNode 节点,它指向我们的 Fiber 树,传入我们的 DOM 元素、设定指向的 Fiber 树,上一篇提过,React 采用双缓冲树的结构,系统中会有 current ( 当前正在展示 ) 和 workInProgress ( 将要更新的 ) 两个fiber树,在初始化的时候,指向 current 树,这里用一个 tag 来表述指向的树, ConcurrentRoot 代表指向 current 树
  • 之后将我们创建的 root 节点挂载到 DOM 上
  • 获取并监听该 DOM 的所有事件
  • 最后返回一个 ReactDOMRoot 对象

我们应该可以理解,createRoot 函数获取了一个 DOM元素,创建了一个 FiberRootNode 节点挂载到这这个 DOM 上,然后绑定了所有的事件,最后返回一个 ReactDOMRoot 对象。这个过程有两个不明确的点,FiberRootNode 节点是什么,以及 ReactDOMRoot 对象是什么,那么我们一一来看这两个结构:

FiberRootNode 的创建

上文中,我们使用 createContainer 来创建了一个 FiberRootNode 对象,它的运行过程如下,首先是 createContainer 函数,它在接收参数后,添加了 hydrate 和 initialChildren 两个属性,之后调用了 createFiberRoot

export function createContainer(containerInfo,tag,hydrationCallbacks,isStrictMode,concurrentUpdatesByDefaultOverride,identifierPrefix,onRecoverableError,transitionCallbacks
) {const hydrate = false;    // 服务端渲染相关const initialChildren = null;    // 初始子节点return createFiberRoot(containerInfo,tag,hydrate,initialChildren,hydrationCallbacks,isStrictMode,concurrentUpdatesByDefaultOverride,identifierPrefix,onRecoverableError,transitionCallbacks)
}

createFiberRoot 则创建了 FiberRootNode 和 HostRootFiber

  • HostRootFiber 就是一个上一篇提到过的 Fiber 元素,它是我们整个 Fiber 树的根,
  • FiberRootNode 的 current 属性指向我们的 HostRootFiber ,也就是说通过返回的 FiberRootNode 就可以得到我们的整个 Fiber 树
  • 而 HostRootFiber 的 stateNode 指向 FiberRootNode ,实现了对DOM的追踪
export function createFiberRoot(containerInfo,tag,hydrate,initialChildren,hydrationCallbacks,isStrictMode,concurrentUpdatesByDefaultOverride,identifierPrefix,onRecoverableError,transitionCallbacks
) {// 创建 FiberRoot const root = new FiberRootNode(containerInfo,tag,hydrate,identifierPrefix,onRecoverableError);// 设置服务端渲染回调 if(enableSuspenseCallback)  {root.hydrationCallbacks = hydrationCallbacks;}// 设置过渡回调if(enableTransitionTracing) {root.transitionCallbacks = transitionCallbacks;}// 创建 HostRootFiber const uninitializedFiber = createHostRootFiber(tag,isStrictMode,concurrentUpdatesByDefaultOverride);// 将 HostRootFiber 挂载到 FiberRoot 的 current 属性上 root.current = uninitializedFiber;// 将 HostRootFiber 的 stateNode 设置为 FiberRoot uninitializedFiber.stateNode = root;// 设置 HostRootFiber 的 memoizedStateif(enableCache) {const initialCache = createCache();retainCache(initialCache);root.pooledCache = initialCache;retainCache(initialCache);const initialState = {element: initialChildren,isDehydrated: hydrate,cache: initialCache};uninitializedFiber.memoizedState = initialState;} else {const initialState:RootState = {element: initialChildren,isDehydrated: hydrate,cache: (null: any)};uninitializedFiber.memoizedState = initialsTate;}// 初始化 HostRootFiber 的更修队列 initializeUpdateQueue(unitializedFiber);return root;
}

整个架构类似这样的结构:

请添加图片描述

最后我们分别来看看两个创建函数做了什么:

createHostRootFiber 函数设置了 React Fiber 架构的工作模式 (Concurrent 模式、严格模式、ConcurrentUpdatesByDefaultMode 模式)和设置性能分析的模式,这些先按下不表,然后通过 createFiber 函数创建了一个 Fiber ,关于 Fiber 的结构在上一篇教程中以及提到了,但是在上文我们没有说明 mod 这个属性的作用,这里我们看到,这个属性是从用户的配置中传入的,随着函数调用一步一步来到我们的 Fiber 中:

export function createHostRootFiber(tag,isStrictMode,concurrentUpdatesByDefaultOverride,
)  {let mode;//设置模式if ( tag === ConcurrentRoot ) {mode = ConcurrentMode;if(isStrictMode === true || createRootStrictEffectsByDefault) {mode |= StrictLegacyMode | StrictEffectsMode;}if( !enableSyncDefaultUpdates || (allowConcurrentByDefault && concurrentUpdatesByDefaultOverride)) {mode |= ConcurrentUpdatesByDefaultMode;}} else {mode = NoMode;}if(enableProfilerTimer && isDevToolsPresent) {mode |= ProfileMode;}return createFiber(HostRoot, null, null, mode);
}

FiberRootNode 中则包含了一些 Fiber 在后续执行的时候会用到的关于调度相关的内容,我们会在后续提到:

function FiberRootNode(containerInfo,tag,hydrate,identifierPrefix,onRecoverableError
) {this.tag = tag;this.containerInfo = containerInfo;    // DOM 容器节点this.pendingChildren = null;    this.current = null;    // Fiber 树this.pingCache = null;this.finishedWork = null;    this.timeoutHandle = noTimeout;this.context = null;this.pendingContext = null;this.callbackNode = null;this.callbackPriority = NoLane;this.eventTimes = createLaneMap(NoLanes);this.pendingLanes = NoLanes;this.suspencedLanes = NoLanes;this.pingedLanes = NoLanes;this.expiredLanes = NoLanes;this.mutableReadLanes = NoLanes;this.entangledLanes = NoLanes;this.hiddenUpdates = createLaneMap(null);this.identifierPrefix = identifierPrefix;this.onRecoverableError = onRecoverableError;if(enableCache) {this.pooledCache = null;this.pooledCacheLanes = NoLanes;}if(supportsHydration) {this.mutableSourceEagerHydrationData = null;}if(enableSuspenseCallback) {this.hydrationCallbacks = null;}this.incompleteTransitions = new Map();if(enableTransitionTracking) {this.transitionCallbacks = null;const transitionLanesMap = (this.transitionLanes = []);for(let i = 0; i < TotalLanes; i++) {pendingUpdatersLaneMap.push(new Set())}}
}

ReactDOMRoot

之后我们来看看返回值的 ReactDOMRoot ,它的创建仅仅是将 _internalRoot 属性指向我们传入的 FiberRootNode ,但在这个ReactDOMRoot 的原型链上,我们添加了两个函数, render 和 unmount

  • render 方法校验了传入的树是不是已经卸载,如果可用则调用 updateContainer 进行渲染操作,这个函数会将 element 结构转为 fiber 树,生成 html 节点渲染到指定的 dom 元素中,并且帮助我们操作 Hooks ,这些都会在后续讲到
  • unmount 方法主要是用来清除数据、卸载 fiber 树
function ReactDOMRoot(internalRoot) {this._internalRoot = internalRoot;
}// 渲染
ReactDOMRoot.prototype.render = function (children) {const root = this._internalRoot;if(root === null) {throw new Error('Cannot update an unmounted root.');}// 渲染 updateContainer(children, root, null, null);
}// 卸载
ReactDOMRoot.prototype.unmount = function(): void {const root = this._internalRoot;if (root !== null) {this._internalRoot = null;const container = root.containerInfo;// 清空 Fiber 树flushSync(() => {updateContainer(null, root, null, null);});// 将 FiberRoot 从 DOM 容器元素上卸载unmarkContainerAsRoot(container);}
};

顺带一提,React 是这样标识一个元素是否被使用的,可用查看上文提到的 markContainerAsRoot 函数:

React 将一个 DOM 元素的一个特殊的属性设置为 FiberRoot 来标识这个 DOM 以及作为一个 root 来使用,如果后续再次将这个 DOM 作为一个 root 传入 render 的话,React 就可以通过这个属性来判断是否是重用,也就是上述的 createRoot 的第二步操作

注意:这个特殊属性要使用随机数是防止和业务代码的属性名冲突,起着类似 symbol 的效果、

而在我们 unmount 这个节点的时候,我们需要把这个属性从 DOM 中删除,这样下次检测的时候,这个 DOM 就不再被判定为一个根节点了

cosnt randomKey = Math.ramdom().toString(36).slice(2);const internalInstanceKey = '' + randomKey;// 在 DOM 节点上设置属性 '_reactFiber' + ramdomKey 为 FiberRoot
export function markContainerAsRoot(hostInst, node) {node[internalInstanceKey] = hostInst;
} 

总结

这一节中,我们主要从 render 函数入口出发,一个 React 的应用被挂载到 DOM 节点上经历了以下的过程:

  • createRoot 函数调用了 createContainer 函数返回一个 ReactDOMRoot
  • createContainer 函数创建了 createFiberRoot
  • createFiberRoot 返回一个 FiberRootNode 指向一个 HostRootFiber ,HostRootFiber 则是我们的 Fiber 树的根节点
  • FiberRootNode 被挂载到 DOM 元素上,用一个随机的标记来唯一标识它
  • ReactDOMRoot 指向了 FiberRootNode ,同时提供了 render 和 unmount 两个方法来渲染和卸载元素
  • 我们将我们的 React 应用的根组件传入 render ,那么我们的应用将挂载在 createRoot 的 DOM 上,并且被解析到刚刚生成的 Fiber 树上(此时只有根节点)

那么经过这一节的内容,我们上一次提问的几个问题里,Fiber 树是怎么样生成和更新的解决了一部分,同时我们有了新的疑问:updateContainer 这个函数怎么样做到将 element 结构转为 fiber 树,以及我们更新我们的 React 应用的时候,怎么样进行更新,不急,我们将逐步来解决这些问题。

相关内容

热门资讯

安卓系统应用数据目录,揭秘系统... 你有没有想过,你的安卓手机里那些应用,它们的数据都藏在哪个角落呢?今天,就让我带你一探究竟,揭开安卓...
kindle 安卓 系统 刷机... 亲爱的读者们,你是不是也和我一样,对电子阅读设备情有独钟?尤其是那款小巧便携的Kindle,简直是阅...
平板 win 安卓 双系统,... 你有没有想过,拥有一台既能运行Windows系统,又能流畅使用安卓应用的多功能平板电脑,是不是能让你...
电脑安卓和苹果系统,电脑操作系... 你有没有发现,现在无论是工作还是娱乐,电脑已经成了我们生活中不可或缺的好伙伴呢!而在这众多电脑中,安...
手机安卓系统下载5.0,引领智... 你有没有发现,最近手机界又掀起了一股热潮?没错,就是安卓系统5.0的下载。这可是安卓家族里的一大亮点...
小森生活系统安卓,打造绿色生态... 你知道吗?最近在手机应用市场上,有个叫做“小森生活系统安卓”的新玩意儿火得一塌糊涂。它就像一个神奇的...
王者荣耀安卓系统更换,畅享全新... 最近是不是发现你的王者荣耀游戏体验有点不对劲?别急,让我来给你揭秘一下安卓系统更换背后的那些事儿!一...
ios系统数据如何导入安卓系统... 你是不是也有过这样的经历:手机里存满了珍贵的照片、视频和联系人,突然有一天,你决定换一台安卓手机,却...
王者荣耀启动安卓系统,畅享指尖... 你知道吗?最近王者荣耀可是大动作连连,竟然宣布要启动安卓系统了!这消息一出,瞬间在游戏圈引起了不小的...
安卓始终显示系统栏,安卓系统栏... 你是不是也遇到了这个问题?手机屏幕上那个讨厌的系统栏,有时候它就像一个顽皮的小鬼,总是赖在你的屏幕上...
苹果系统可以装在安卓,探索跨平... 你知道吗?最近在科技圈里可是掀起了一股热潮呢!那就是——苹果系统竟然可以装在安卓设备上!是不是听起来...
安卓系统双清目的,安卓系统双清... 你知道吗?最近在安卓系统圈子里,有个话题可是热得不得了,那就是“双清”。别小看这个“双清”,它可是关...
安卓系统台电平板,畅享智能生活... 你有没有发现,最近身边的朋友都开始讨论起一款叫做台电的安卓系统平板电脑呢?这可不是随便说说,这款平板...
三菱安卓系统,智能科技与驾驶体... 亲爱的读者,你是否曾好奇过,那些在我们生活中默默无闻的汽车品牌,它们是如何将科技与驾驶体验完美结合的...
安卓系统为什么好,引领智能生活... 你有没有发现,身边的朋友、同事,甚至家人,几乎人手一台安卓手机?这可不是偶然现象哦!安卓系统,这个来...
安卓如何改键盘系统,Andro... 你是不是也和我一样,对安卓手机的键盘系统有点儿不满意?想要换一个更顺手的键盘,但又不知道怎么操作?别...
怎么升级安卓14系统,解锁安卓... 你有没有发现,你的安卓手机最近是不是有点儿慢吞吞的?别急,别急,升级到安卓14系统,让你的手机焕发新...
安卓手机如何系统内录,轻松生成... 你有没有想过,有时候想要记录下手机里的精彩瞬间,却发现没有合适的工具?别急,今天就来教你怎么在安卓手...
最绚丽的安卓系统,最绚丽版本全... 哇,你知道吗?在安卓的世界里,有一款系统,它就像是一颗璀璨的明珠,闪耀着最绚丽的色彩。它就是——最绚...
小米系统安卓通知权限,深度解析... 亲爱的手机控们,你是否曾为手机通知栏里乱糟糟的信息而烦恼?又或者,你是否好奇过,为什么有些应用总是能...