资讯动态

ReactDOMRoot源码解析:从createRoot到render的完整链路

发布时间:2026/10/13 4:37:11 来源:尧图企业网站定制
做React源码分析很多人一上来就啃scheduler调度器或者fiber架构结果被lane、workLoop这些概念劝退。我自己的经验是不如先从ReactDOMRoot入手——这是每个React应用真正的起点你天天写的createRoot(...).render(App/)本质上就是在操作这个类的实例。搞清楚ReactDOMRoot怎么实现后面再看update、reconcile、commit这条主链路会顺畅非常多。这篇源码分析我会直接对照React 18.x的react-dom源码把createRoot入口、ReactDOMRoot构造函数、render/unmount方法、FiberRootNode和HostRoot fiber的关系一条线捋清楚。内容适合两类人看一类是准备React面试想搞懂createRoot返回的到底是什么、root.render背后发生了什么的另一类是刚开始做React源码分析需要一个低门槛切入点的。读完你能回答几个硬核问题ReactDOMRoot和FiberRootNode到底什么关系root.render为什么叫入队一个更新unmount之后为什么不能再render。1. ReactDOMRoot在React体系中的定位1.1 为什么会有createRoot这个APIReact 18之前大家启动应用只有一个选择ReactDOM.render(App/, document.getElementById(root))。那时候React的根节点概念是有的但API层面没有单独暴露出来ReactDOM.render内部会自己搞一套legacy root的逻辑。到了React 18官方把并发渲染正式推到台前createRoot成了标配入口ReactDOM.render被标记为legacy还带着你需要主动绕过的警告。这个变化背后不只是API搬家。createRoot对应的root类型是ConcurrentRoot而ReactDOM.render内部创建的是LegacyRoot两者的fiber tag不一样调度行为和优先级处理也有区别。并发特性如startTransition、可中断渲染、自动批处理本质上都需要一个支持并发模式的root来承载。所以React 18把client API拆分到react-dom/client并强调要用createRoot是在源码层面做了一场结构性区分老的同步渲染路径保留兼容新的并发渲染路径正式转正。我实际看源码的时候发现一个有意思的点即使是React 18里你强行用ReactDOM.render它内部也会创建一个ReactDOMRoot实例只不过tag是LegacyRoot。所以ReactDOMRoot并不是只有createRoot在用它是所有render路径共同的门面对象。理解了这一层再看整个react-dom的入口代码就能明白为什么源码里大量逻辑都在区分root.tag ConcurrentRoot还是LegacyRoot。1.2 ReactDOMRoot实际负责什么ReactDOMRoot这个类从代码量看极其短小React 18源码里构造函数加两个原型方法总共不超过四十行。但它的职责非常关键概括起来是三件事第一持有根节点数据。构造函数把传入的FiberRootNode存到this._internalRoot这里的FiberRootNode是整个fiber树挂在容器上的根锚点ReactDOMRoot是暴露给用户的门面对象内部数据全靠_internalRoot间接访问。第二接收React元素入队。root.render(children)做的事情不是直接操作DOM而是把传入的React元素包装成一个update压入HostRoot fiber的更新队列再触发一次调度让React自己决定什么时候去渲染。第三提供卸载入口。root.unmount()会入队一个空更新通知React把整棵fiber树销毁并清理root容器上的内部标记。一句话总结ReactDOMRoot是用户侧操作React根节点的遥控器真正干活的引擎在FiberRootNode和fiber架构里。很多面试者会在这两个概念上卡壳后面第四章我再仔细拆。2. createRoot入口从DOM容器到ReactDOMRoot2.1 createRoot函数逐行拆解createRoot定义在react-dom/src/client/ReactDOMRoot.js里源码不长但是信息密度很高。核心流程是校验容器、解析options、创建fiber root、给容器打标记、绑定事件、返回ReactDOMRoot实例。我按源码顺序带你走一遍export function createRoot(container, options) { if (!isValidContainer(container)) { throw new Error(createRoot(...): Target container is not a DOM element.); } warnIfReactDOMDirIncreateRoot(container); let isStrictMode false; let concurrentUpdatesByDefault false; let identifierPrefix ; let onUncaughtError noop; let onCaughtError noop; let onRecoverableError noop; let transitionCallbacks null; if (options ! null options ! undefined) { // 逐项读取配置 if (options.unstable_strictMode true) { isStrictMode true; } // ... } const root createContainer( container, ConcurrentRoot, null, isStrictMode, concurrentUpdatesByDefault, identifierPrefix, onUncaughtError, onCaughtError, onRecoverableError, transitionCallbacks, ); markContainerAsRoot(root.current, container); const rootContainerElement container.nodeType COMMENT_NODE ? container.parentNode : container; listenToAllSupportedEvents(rootContainerElement); return new ReactDOMRoot(root); }可以看到createRoot本身并没有做太多渲染相关的事情。它先校验容器然后读取options参数接着调用createContainer创建真正的root数据结构然后两个关键动作markContainerAsRoot给DOM容器打上React内部标记listenToAllSupportedEvents在容器上绑定事件委托。最后return new ReactDOMRoot(root)把内部root包装成对外实例。一个经常被忽视的细节markContainerAsRoot和listenToAllSupportedEvents都在new ReactDOMRoot之前执行说明DOM容器在用户拿到root之前就已经被React接管了。2.2 参数校验和options里值得注意的细节isValidContainer这段很多人都只是扫一眼就过了其实它透露了React对容器的兼容边界export function isValidContainer(node) { return !!( node (node.nodeType ELEMENT_NODE || node.nodeType DOCUMENT_NODE || node.nodeType DOCUMENT_FRAGMENT_NODE || (node.nodeType COMMENT_NODE node.nodeType COMMENT_NODE (node: any).text react-mount-point-unstable )) ); }普通开发者只需要知道传入一个div就行但源码里允许了四种情况元素节点、document节点、fragment节点、特定注释节点。最后那个comment节点是为了兼容老版本ReactDOM.render曾经支持的特殊挂载方式现在的createRoot很少会用到但校验逻辑还是保留了。options参数在React 18里还有个名字很劝退的unstable_strictMode官方从React 18.3开始才正式把StrictMode在根节点上启用的方式稳定下来。如果你想在根上开严格模式可以传React.StrictMode包裹组件也可以在createRoot时传unstable_strictMode: true。源码里还会处理identifierPrefix、onUncaughtError、onCaughtError、onRecoverableError这些配置它们会一路传给FiberRootNode作为运行时的错误处理回调。还有一个我翻源码时觉得挺有意思的冷知识markContainerAsRoot在DOM节点上挂的属性key不是固定的__reactContainer而是带一段随机字符串const randomKey Math.random().toString(36).slice(2); export const internalContainerInstanceKey __reactContainer$ randomKey;每个应用加载时这个randomKey都不一样。这样设计是为了避免多个React副本同时存在时互相污染标记。React DevTools能快速定位root节点靠的也是这个属性。你在浏览器里选中#root在Elements面板的属性栏能看到一个__reactContainer$xxx字段那就是React给你的容器盖的章。2.3 createContainer到createFiberRoot的建根流程createContainer定义在ReactFiberReconciler.js里它内部只是转调createFiberRoot。createFiberRoot的流程可以概括为三步创建FiberRootNode、创建HostRoot fiber、初始化更新队列。export function createFiberRoot( containerInfo, tag, hydrate, initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefault, identifierPrefix, onUncaughtError, onCaughtError, onRecoverableError, transitionCallbacks ) { const root new FiberRootNode( containerInfo, tag, hydrate, identifierPrefix, onUncaughtError, onCaughtError, onRecoverableError, transitionCallbacks ); const uninitializedFiber createHostRootFiber(tag, isStrictMode, concurrentUpdatesByDefault); root.current uninitializedFiber; uninitializedFiber.stateNode root; initializeUpdateQueue(uninitializedFiber); return root; }这段代码揭示了一个关键结构FiberRootNode和HostRoot fiber互相引用。FiberRootNode的current指向当前的HostRoot fiber而HostRoot fiber的stateNode指回FiberRootNode。整个fiber树由此有了一个稳定的根对。createHostRootFiber还会初始化memoizedState初始值大概是这样的结构const initialState { element: initialChildren, // createRoot时是null isDehydrated: hydrate, // createRoot时是false cache: null, transitions: null, };initializeUpdateQueue则给HostRoot fiber挂上更新队列初始baseState就是上面那个initialState。这为后面root.render入队第一个更新做好了准备。3. ReactDOMRoot类内部render与unmount的实现3.1 构造函数内部root的存放与null判空React 18里ReactDOMRoot的定义是这样的function ReactDOMRoot(internalRoot) { this._internalRoot internalRoot; }构造函数极简就是把createRoot创建的FiberRootNode存到实例上赋值给this._internalRoot。很多人会困惑为什么叫_internalRoot因为对外这是一个私有字段虽然JS没有真正私有但下划线前缀是告诉调用者别动它这是React内部引擎要用的。你在控制台打印root对象能看到_internalRoot下面挂着一大堆字段那就是FiberRootNode的完整数据。为什么不用普通对象而用构造函数加原型核心原因是结构清晰加方法共享。render和unmount这两个方法定义在prototype上所有ReactDOMRoot实例共享同一份实现避免每个root都复制一份方法。同时构造函数的形式也方便做instanceof判断。3.2 render方法从root.render到scheduleUpdateOnFiber的完整链路ReactDOMRoot.prototype.render源码非常短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); };关键的判断是root null。什么时候_internalRoot会是null只有调用了unmount之后。所以unmount之后不能再render不是约定俗成而是源码层面直接抛错。真正干活的是updateContainer这个函数在ReactFiberReconciler.js里逻辑链路很长我拆成几步export function updateContainer(element, container, parentComponent, callback) { const current container.current; const lane requestUpdateLane(current); const context getContextForSubtree(parentComponent); if (container.context null) { container.context context; } else { container.pendingContext context; } const update createUpdate(lane); update.payload { element }; callback callback undefined ? null : callback; if (callback ! null) { update.callback callback; } const root enqueueUpdate(current, update, lane); if (root ! null) { scheduleUpdateOnFiber(root, current, lane); entangleTransitions(root, current, lane); } return lane; }第一步requestUpdateLane根据当前调度环境申请一条优先级lane。React并发模式里每次更新都要有一个优先级等级紧急更新拿同步lane过渡更新拿transition lane。第二步createUpdate(lane)创建一个update对象把root.render(children)传入的React元素塞进update.payload { element }。这里有个很多人忽略的重点root.render的参数被包成{ element: children }而不是直接用children作为payload。因为React元素本身只是描述真正渲染时需要把它当成fiber节点的element属性去消费。第三步enqueueUpdate(current, update, lane)把update插入到HostRoot fiber的更新队列。这个队列不是普通数组而是一个环形链表结构。每次插入都放在shared.pending上pending永远指向最后一个update而pending.next指向第一个。这种环形链表的好处是O(1)插入并且处理时能快速拿到第一批更新。第四步scheduleUpdateOnFiber(root, current, lane)触发调度。从这里开始React会安排一次渲染任务可能同步执行也可能被调度器延后到合适时机然后经过render阶段、commit阶段最终把DOM变化同步到页面上。你可以看到root.render本身没有任何DOM操作。它只是把一个更新塞进队列再通知调度器有事做了。这也是理解React并发特性的关键从root.render到DOM更新中间是异步可中断的。3.3 unmount方法清引用、入队空更新、清事件标记unmount源码同样精简ReactDOMRoot.prototype.unmount function () { const root this._internalRoot; if (root ! null) { this._internalRoot null; const container root.containerInfo; updateContainer(null, root, null, null); flushSyncCallbacks(); markContainerAsRoot(undefined, container); } };拆开看unmount做了四件事第一判空。如果_internalRoot已经为null说明已经卸载过直接什么都不做。这保证了unmount的幂等性。第二把this._internalRoot null。这一步非常关键它让后续任何尝试调用render的操作都会走到前面说的Cannot update an unmounted root分支从实例层面杜绝了卸载后继续渲染的可能。第三调用updateContainer(null, root, null, null)。注意这里传的是null而不是undefined语义是把root里的元素更新为nullReact在render阶段发现element为null就会把整棵树清空卸载。这一步是异步调度的所以紧跟后面调用了flushSyncCallbacks()强制把同步回调清掉确保DOM清理尽快完成。第四markContainerAsRoot(undefined, container)把容器上那个__reactContainer$xxx标记清除通知外界这个容器不再是React的root了。一个值得注意的地方unmount并不会立刻同步清空DOM。它入队的更新需要调度器处理flushSyncCallbacks只是让已经排队的同步任务尽快执行。如果在一个复杂的调度环境下真正DOM被拿掉可能会有短暂延迟。但实践里感知不明显我们通常认为unmount之后页面内容就被移除了。3.4 React 18与React 19在实现上的差异React 18里ReactDOMRoot是典型的构造函数加原型方法。但到了React 19源码里做了一次重构把render和unmount的逻辑收进了内部工厂函数createRootInternals再挂到原型上。简单说方法本体变成了工厂函数返回的一部分function createRootInternals() { function render(children) { const root this._internalRoot; if (root null) { throw new Error(Cannot update an unmounted root.); } updateContainer(children, root, null, null); } function unmount() { const root this._internalRoot; if (root ! null) { this._internalRoot null; const container root.containerInfo; updateContainer(null, root, null, null); flushSyncCallbacks(); markContainerAsRoot(undefined, container); } } return { render, unmount }; }这样调整背后的动机有两个一是后续有其他内部API需要复用同一套render/unmount逻辑二是让ReactDOMRoot的构造更灵活不再绑定prototype的写法。你要是看React 19的hydrateRoot实现会发现它也复用了这套内部方法。所以源码分析别只看一个版本的实现版本差异往往暴露了设计意图的演进。4. ReactDOMRoot的地基FiberRootNode与HostRoot fiber4.1 FiberRootNode关键字段FiberRootNode是react-reconciler层的数据结构整个应用的fiber树、更新优先级、渲染进度都记录在它身上。我挑几个最重要的字段给你梳理一遍。第一个是tag标记root类型。createRoot创建出来的是ConcurrentRootReactDOM.render创建的是LegacyRoot。很多渲染逻辑分支都靠这个tag判断比如是否允许并发更新、是否走hydration逻辑。第二个是containerInfo存的是createRoot传入的DOM容器。ReactDOMRoot的unmount方法里拿root.containerInfo就是这个字段。第三个是current指向当前HostRoot fiber。React的双缓冲机制依赖这个指针渲染完成前新树挂在workInProgress上提交时切换current。第四个是pendingLanes和pendingUpdateLanes记录等待处理的更新优先级。lane模型是并发调度的核心React用31位的位向量表示优先级多个lane可以同时存在。root.render触发更新时会让某些lane置位。第五个是finishedWork指向已经完成render阶段、等待提交的fiber树。从render阶段到commit阶段交接靠的就是这个字段。第六个是context和pendingContext保存root级别的React context默认值通常是null。当有新的context提供者出现在根节点上层时React会把新context暂存在pendingContext在渲染开始时切换。这些字段在new FiberRootNode构造函数里有一个超长的初始化列表第一次看源码很容易被吓到。但理解核心几个就够了current、containerInfo、pendingLanes、finishedWork。4.2 HostRoot fiber与初始化状态HostRoot fiber是整棵fiber树的根节点它的tag是HostRoot数字标识是3。这个fiber比较特殊它没有对应的DOM元素stateNode指向FiberRootNode作用就是作为fiber树的起点节点。初始化时HostRoot fiber的memoizedState是一个对象{ element: null, // 等待渲染的React元素root.render后这里会被更新 isDehydrated: false, // 是否处于服务端渲染水合状态 cache: null, // 缓存相关数据 transitions: null, // 过渡更新状态 }updateQueue由initializeUpdateQueue创建初始baseState就是上面那个对象。每次root.renderReact都会在这个队列里追加一个update然后由render阶段把队列里的element取出来生成或更新子fiber。如果你在控制台手动修改过root._internalRoot.current.memoizedState会发现页面渲染结果并不会立刻变化因为fiber数据不是直接驱动DOM的。真正驱动DOM的是render阶段从updateQueue里计算出的新memoizedState再经过commit阶段提交。这也是很多人调试React内部状态时容易误解的地方。4.3 两个root的关系与常见理解误区ReactDOMRoot和FiberRootNode之间的关系可以类比成遥控器和电视机。ReactDOMRoot是用户手里的遥控器小巧直观你只需要按render、unmount两个按钮FiberRootNode是电视机内部的电路板复杂但承担所有实际工作。两者的连接线就是_internalRoot字段。常见误区有三个第一个误区认为ReactDOMRoot就是fiber树根。实际上fiber树根是HostRoot fiberFiberRootNode只是持有它并记录调度状态的容器。ReactDOMRoot更外层它连fiber树根都不是只是一个客户端API层面的门面。第二个误区认为root.render会立刻触发DOM操作。前面已经拆解过root.render只是入队更新并触发调度DOM变化发生在commit阶段且并发模式下中间可能被中断、重来。第三个误区认为unmount会销毁整个root数据结构。源码里unmount只是把_internalRoot置null、入队空更新、清除DOM标记FiberRootNode对象本身并没有被delete只是脱离了ReactDOMRoot的引用。真正的内存回收交给GC处理。理清这三个误区React的启动流程就清晰了一大半。5. 面试与实战ReactDOMRoot相关考点和排坑记录5.1 高频面试题与答题要点React源码相关的面试题ReactDOMRoot这个概念出现的频率很高。我整理几个常见问题配了答题思路你可以在面试前快速过一遍。面试题核心答题思路createRoot返回的是什么对象ReactDOMRoot实例内部持有_internalRoot指向FiberRootNodeFiberRootNode和HostRoot fiber是什么关系互相引用FiberRootNode.current指向HostRoot fiberHostRoot fiber.stateNode指回FiberRootNoderoot.render到底做了什么创建update、挂到HostRoot更新队列、调用scheduleUpdateOnFiber触发调度不直接操作DOM为什么React 18要用createRoot替代ReactDOM.rendercreateRoot创建ConcurrentRoot支持并发特性和可中断渲染ReactDOM.render是LegacyRoot语义unmount后再调用render会怎样抛错Cannot update an unmounted root.因为_internalRoot已被置为nullReact事件为什么会代理到root容器上createRoot里调用了listenToAllSupportedEvents把事件委托绑定到root容器而不是document回答问题的时候不要只背结论。面试官更想听到的是你对入队更新、调度这些词的理解能说出updateContainer - scheduleUpdateOnFiber这条链路基本就证明你不是死记硬背。5.2 业务中常见的五个坑我在实际项目里见过不少和根节点相关的线上问题列出来给大家避坑。第一个坑对同一个DOM元素重复调用createRoot。React会警告Attempted to recreate root而且行为不可控。正确的做法是模块顶层只创建一次root后续更新都通过root.render。第二个坑在createRoot之后再用innerHTML或replaceChildren清空容器。React对DOM的掌控是基于它自己挂载的标记和fiber树如果你手动把容器内容换掉React提交阶段可能把DOM状态搞乱。清空内容请用root.render(null)销毁请用root.unmount。第三个坑unmount之后又调用render。前面源码已经说了这个直接抛异常。常见场景是热更新、组件卸载后又尝试重新挂载需要判断root是否已被unmount。第四个坑StrictMode下副作用执行两次。React 18的StrictMode会让组件mount时多执行一次初始化流程开发模式下正常许多新手以为代码写错了。根节点用React.StrictMode包裹后render阶段函数会被调用两次这是故意的目的是暴露副作用问题。第五个坑依赖document级事件委托。React 17之前合成事件挂在document上React 17开始挂在root容器上。如果你在document上监听React相关的原生事件可能会漏掉冒泡到root但没继续往上冒泡的情况。这就是为什么根容器不要嵌套在事件监听的中间层。5.3 源码调试技巧看源码不能光靠脑子过断点和打印才是效率工具。第一个调试点在ReactDOMRoot.prototype.render里打断点可以看到每一次root.render调用的入参再单步进updateContainer就能观察update对象的创建和入队。我是用Chrome DevTools直接引源码map文件来断的搜ReactDOMRoot.js就能定位。第二个调试点在scheduleUpdateOnFiber打断点能看到调度器如何接手更新。这里你可以观察lane的值和root.pendingLanes的变化对理解优先级有帮助。第三个打印技巧浏览器控制台里选中root容器查看它的__reactContainer$xxx属性。这个属性指向HostRoot fiber等于在真实DOM上直接嗅探React内部结构。你还可以通过这个属性反向验证两个root之间的连接关系。第四个技巧用React DevTools看root节点。Components面板最顶层的那个节点就是root对应的fiber点开能看到它的props和hooks但对理解数据结构帮助有限不如直接控制台打印直观。6. 手写一个简化版ReactDOMRoot6.1 最小可用实现理解源码的最好方式是自己写一遍。我在这里放一个简化版ReactDOMRoot去掉了lane调度、diff、commit这些复杂部分只保留核心链路createRoot - 创建fiber root - ReactDOMRoot实例 - render入队 - 触发调度。const randomKey Math.random().toString(36).slice(2); const containerKey __reactContainer$ randomKey; function createRoot(container) { if (!container || (container.nodeType ! 1 container.nodeType ! 9 container.nodeType ! 11)) { throw new Error(createRoot(...): Target container is not a DOM element.); } const fiberRoot createFiberRoot(container); container[containerKey] fiberRoot.current; return new ReactDOMRoot(fiberRoot); } function createFiberRoot(container) { const fiberRoot { containerInfo: container, current: null, pendingLanes: 0, finishedWork: null, }; const hostRootFiber { tag: 3, // HostRoot stateNode: fiberRoot, memoizedState: { element: null, isDehydrated: false }, updateQueue: null, child: null, sibling: null, return: null, }; hostRootFiber.updateQueue createUpdateQueue(hostRootFiber.memoizedState); fiberRoot.current hostRootFiber; return fiberRoot; } function createUpdateQueue(baseState) { return { baseState, firstBaseUpdate: null, lastBaseUpdate: null, shared: { pending: null }, callbacks: null, }; } class ReactDOMRoot { constructor(internalRoot) { this._internalRoot internalRoot; } render(children) { const root this._internalRoot; if (root null) { throw new Error(Cannot update an unmounted root.); } updateContainer(children, root); } unmount() { const root this._internalRoot; if (root ! null) { this._internalRoot null; root.containerInfo[containerKey] undefined; updateContainer(null, root); } } } function updateContainer(element, container) { const current container.current; const update { payload: { element }, next: null }; enqueueUpdate(current, update); scheduleUpdateOnFiber(container, current); } function enqueueUpdate(fiber, update) { const queue fiber.updateQueue; const pending queue.shared.pending; if (pending null) { update.next update; // 环形链表自己指向自己 } else { update.next pending.next; pending.next update; } queue.shared.pending update; } function scheduleUpdateOnFiber(fiberRoot, currentFiber) { // 简化版直接把当前fiber标记为需要更新并触发一次同步渲染 // 真实实现会走lane分配、调度器、render阶段、commit阶段 console.log(schedule update on fiber:, currentFiber.tag); // 这里只模拟入队后的调度信号真正的渲染循环省略 const queue currentFiber.updateQueue; let pending queue.shared.pending; if (pending) { const first pending.next; queue.baseState { ...queue.baseState, element: first.payload.element }; queue.shared.pending null; } }这段代码可以跑通基本流程createRoot往容器上挂标记、ReactDOMRoot持有内部root、render创建update并入队、schedule里简单消费update。你可以在浏览器里跑一下查看容器的__reactContainer$xxx属性对比真实React的行为。但对真实的DOM渲染这个版本完全没实现它只是一个理解源码结构的骨架。6.2 与真实实现的核心差距我写这个简化版的过程里反复对照真实源码发现差距主要集中在六个方面。第一lane优先级模型。真实源码里每次更新都要通过requestUpdateLane分配具体优先级整个调度系统围绕lane展开。我的简化版把所有更新都当成同等优先级无法实现可中断和并发。第二调度器。真实实现里scheduleUpdateOnFiber会触发scheduler调度任务可能被安排到宏任务执行可中断可恢复。我的版本直接同步消费update完全没有安排到什么时候执行的概念。第三render阶段。真实源码从HostRoot出发递归构建整棵fiber树执行render函数、diff子节点。我的版本只更新了根节点的element压根没有子fiber的构建。第四commit阶段。真实源码会先收集副作用再统一提交DOM变更包括DOM插入、删除、更新、ref绑定等。我的版本没有任何DOM操作。第五hydration。服务端渲染的hydrateRoot会走完全不同的初始化路径HostRoot的isDehydrated会变成truerender阶段要复用已有DOM节点。我的版本完全没处理。第六事件系统。真实源码在createRoot时会调用listenToAllSupportedEvents把一大堆原生事件委托到root容器。我的版本没有任何事件绑定。手写这个简化版最大的价值不在于实现功能而在于让你在动手过程中建立根节点数据流的心智模型。等你带着这个模型再回去读真实源码那些散布在ReactFiberReconciler、ReactFiberWorkLoop里的函数就不再是零散的碎片了。我个人这些年看React源码的体会是ReactDOMRoot这个入口是最适合普通开发者切入的点。它不像scheduler那样纯粹抽象也不像reconciler那样函数庞大。它处在一个用户侧和引擎侧的接缝位置看得见摸得着。我第一次把断点打在ReactDOMRoot.prototype.render顺藤摸瓜看到updateContainer里的createUpdate和enqueueUpdate对React的更新概念一下子就从重新渲染组件升级成了入队一个带优先级的任务。最后再分享一个实用小技巧分析这类源码时直接把源码文件下载下来配合断点调试比自己盲读效率高很多你甚至可以在关键函数里临时加console.log观察真实应用的调度轨迹。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑