前言
- 本文源码版本是React 17.0,源码分支为feature/createRoot,分析的模式是
Concurrent(并发)模式,且对一些不太重要的源码进行了删除 - 要想搞清楚
React源码,debugger是必不可少的,否则看再多的源码解析,还是只能停留于感性认识,无法真正发展到理性认识
入口
我们一般会在项目的src/index.ts文件中初始化项目,即创建根节点后将App组件渲染到根节点上
import React from 'react';
import ReactDOM from 'react-dom';
function App() {
return <div>app</div>
}
const root = document.getElementById('root')
// Concurrent mode
ReactDOM.createRoot(root).render(<App />);
那ReactDOM.createRoot在这一过程究竟做了什么呢?怀着好奇心,我们从ReactDOM.createRoot入口开始debugger,来一探究竟
createRoot
export function createRoot(container: Container): RootType {
return new ReactDOMRoot(container);
}
首先自然是来到 createRoot 函数,createRoot接收一个container,即div#root,而其实际上返回的是一个ReactDOMRoot实例
ReactDOMRoot
function ReactDOMRoot(container: Container) {
// 这里采用的是并发模式
this._internalRoot = createRootImpl(container, ConcurrentRoot);
}
而创建 ReactDOMRoot 实例仅仅是通过createRootImpl创建一个rootContainer,赋值给_internalRoot属性上
这里我们还注意到上面传入了一个ConcurrentRoot,这里简单介绍一些。 React中的模式有三种模式:
// 传统模式
export const LegacyRoot = 0;
// 渐进模式
export const BlockingRoot = 1;
// 并发模式
export const ConcurrentRoot = 2;
传统模式我们的写法是:
ReactDOM.render(<App />, document.getElementById('root'))
本文的模式是Concurrent Mode,其写法是:
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
BlockingRoot则是一个介于LegacyRoot和ConcurrentRoot之间的过渡模式,这里我们略过
那我们可以大概猜测到,实际上的主要实现应该都是在接下来的createRootImpl
createRootImpl
createRootImpl 中impl的全称为implement,表示实现的意思,这在React源码中随处可见,如createPortalImpl、mountEffectImpl、updateEffectImpl、commitRootImpl等等
function createRootImpl(
// container即div#root
container: Container,
tag: RootTag,
) {
// 第一步:这里的tag对应ConcurrentRoot(ConcurrentRoot = 2),即创建一个并发模式的root
const root = createContainer(container, tag);
// 第二步:将创建的root.current挂载到container的`__reactContainer$${randomKey}`属性上
markContainerAsRoot(root.current, container);
// 获取container节点的类型,对于div#root,其nodeType为1
const containerNodeType = container.nodeType;
// COMMENT_NODE = 8,代表注释,nodeType = 1, 代表元素
// nodeType详细可看https://www.w3school.com.cn/jsref/prop_node_nodetype.asp
const rootContainerElement =
containerNodeType === COMMENT_NODE ? container.parentNode : container;
// 第三步:在div#root上绑定各种事件,包括捕获和冒泡阶段
listenToAllSupportedEvents(rootContainerElement);
return root;
}
createRootImpl主要做了三件事
1. 通过 createContainer 创建一个并发模式的fiberRoot给root
export function createContainer(
containerInfo: Container,
tag: RootTag,
): OpaqueRoot {
return createFiberRoot(containerInfo, tag);
}
而createContainer中仅仅将createRootImpl传入的container(div#root)和tag(ConcurrentRoot)传给 createFiberRoot
export function createFiberRoot(
// containerInfo就是根节点,如<div id='app'></div>
containerInfo: any,
// 如ConcurrentRoot模式
tag: RootTag,
): FiberRoot {
// 创建fiberRootNode
const root: FiberRoot = new FiberRootNode(containerInfo, tag);
// 创建rootFiber
const uninitializedFiber = createHostRootFiber(tag);
// root.current指向rootFiber,root.current指向哪棵Fiber树,页面上就显示该Fiber树对应的dom
root.current = uninitializedFiber;
// rootFiber.stateNode指向FiberRoot,可通过stateNode.containerInfo取到对应的dom根节点div#root
uninitializedFiber.stateNode = root;
// 初始化updateQueue,对于RootFiber,queue.share.pending上面存储着element
initializeUpdateQueue(uninitializedFiber);
return root;
}
我们可以看到,最终的root是来自 FiberRootNode 实例
// 以下只展示一些相关的属性,没展示的用...忽略掉
function FiberRootNode(containerInfo, tag, hydrate) {
// type RootTag = 0 | 1 | 2;
// const LegacyRoot = 0;
// const BlockingRoot = 1;
// const ConcurrentRoot = 2;
// 根节点类型
this.tag = tag;
// 存储dom 根节点,如<div id='app'></div>
this.containerInfo = containerInfo;
// 指向上面的RootFiber(uninitializedFiber)
this.current = null;
...
}
然后根据tag(这里是ConcurrentRoot),通过 createHostRootFiber 创建RootFiber
export function createHostRootFiber(tag: RootTag): Fiber {
let mode;
if (tag === ConcurrentRoot) {
// StrictMode = 0b00001;
// BlockingMode = 0b00010;
// ConcurrentMode = 0b00100;
// 从这里可以看出,ConcurrentRoot下会开启严格模式
mode = ConcurrentMode | BlockingMode | StrictMode; // 0b00111
} else {...}
...
const newFiber = createFiber(HostRoot, null, null, mode);
return newFiber
}
const createFiber = function(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
const fiberNode = new FiberNode(tag, pendingProps, key, mode);
return fiberNode
};
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
/** Fiber对应组件的类型 Function/Class/HostComponent(如div)/HostText(html中的纯文本)等 */
this.tag = tag;
/** 模式,如ConcurrentRoot模式 */
this.mode = mode;
/** 指向父级 */
this.return = null;
/** 指向第一个child */
this.child = null;
/** 对应HostComponent,即dom对应的Fiber,containerInfo指向该dom */
this.containerInfo = null
...
}
从上面代码可看出最终是通过new了一个 FiberNode 实例生成一个Fiber,这里对应的就是rootFiber(带有RootTag的Fiber),然后root(FiberRoot)的current指向rootFiber
// 创建rootFiber
const uninitializedFiber = createHostRootFiber(tag);
// root.current指向rootFiber,root.current指向哪棵Fiber树,页面上就显示该Fiber树对应的dom
root.current = uninitializedFiber;
而rootFiber.stateNode又指向了root(FiberRoot)
// rootFiber.stateNode指向FiberRoot,可通过stateNode.containerInfo取到对应的dom根节点div#root
uninitializedFiber.stateNode = root;
两者的相互指向如图
之后构建出的Fiber树相互指向如下:
!注意,上面的
fiberRoot和rootFiber可不是同个东西(笔者私以为React里面这样的命名很容易让初学者混淆🐶,所以这里特别强调一下),fiberRoot的类型是FiberRoot,通过new FiberRootNode生成,fiberRoot通过改变current指针指向来渲染不同的页面。而rootFiber是通过new FiberNode生成,与App、div等都有对应的Fiber节点,共同构成一棵Fiber树
createFiberRoot最后一步是通过 initializeUpdateQueue 初始化rootFiber.updateQueue
// 初始化updateQueue,对于RootFiber,queue.share.pending上面存储着React.element,
// 即ReactDOM.createRoot(root).render(<App />);中的<App/>生成的element
initializeUpdateQueue(uninitializedFiber);
这里的updateQueue.share.pending之后会指向一个update,其结构为:
const update: Update<*> = {
// 任务时间,通过performance.now()获取的毫秒数
eventTime,
// 更新优先级
lane,
// 表示更新是哪种类型(UpdateState,ReplaceState,ForceUpdate,CaptureUpdate)
tag: UpdateState,
// payload:更新所携带的状态。
// 在类组件中,有两种可能,对象({}),和函数((prevState, nextProps):newState => {})
// 根组件中,为React.element,即ReactDOM.render的第一个参数
payload: null,
// setState的回调
callback: null,
// 指向下一个update
next: null,
};
该update的payload存储最开始ReactDOM.createRoot(root).render(<App />)中<App/>对应的React.element(由React.createElement生成),即
{
$$typeof: Symbol(react.element)
key: null
props: {}
ref: null
type: App()
_owner: null
}
即 createContainer通过调用createFiberRoot创建了一个fiberRoot,再通过createHostRootFiber创建了一个rootFiber(ConcurrentRoot),前者通过current指向后者,而后者又通过stateNode指向前者,最后就是初始化updateQueue.share.pending为<App/>对应的React.element
2. 将rootFiber(root.current) 绑定到对应的dom节点'__reactContainer$' + randomKey属性上
其中randomDomKey通过Math.random..toString(36).slice(2)生成。这里通过生成一个带有__reactContainer$的random的key,目的也是为了避免覆盖dom上的原有属性和避免被开发者覆盖(因为我们一般在dom节点上添加属性也不会那么巧合命名一个类似__reactInternalInstance$i021muegffg这么复杂的key,除非刻意而为之)
markContainerAsRoot(root.current, container);
export function markContainerAsRoot(hostRoot: Fiber, node: Container): void {
// internalContainerInstanceKey = `__reactContainer$${randomKey}`
node[internalContainerInstanceKey] = hostRoot;
}
因此,对于React的dom根节点,我们都可以在控制台通过获取div#root的 __reactContainer$randomKey 来获取对应的fiber(其他dom节点通过__reactFiber$randomKey获取),这里以知乎为例:
在div#root标签上右键点击Store as global variable后会在控制台自动生成对应的dom,再通过输入前缀__reactContainer$就会自动提示对的key,从而得到对应的fiber
3. 在根节点上监听各种事件,如click、scroll
listenToAllSupportedEvents 顾名思义就是将所有支持的事件绑定到对应的dom节点上,这里即div#root上
// 获取container节点的类型,对于div#root,其nodeType为1
const containerNodeType = container.nodeType;
// COMMENT_NODE = 8,代表注释,nodeType = 1, 代表元素
// nodeType详细可看https://www.w3school.com.cn/jsref/prop_node_nodetype.asp
const rootContainerElement =
containerNodeType === COMMENT_NODE ? container.parentNode : container;
// 第三步:在div#root上绑定各种事件,包括捕获和冒泡阶段
listenToAllSupportedEvents(rootContainerElement);
我们可以通过控制台点击对应dom(这里我们说的是div#root) 的Event Listeners一栏来看看上面绑定了什么事件监听
对于click、change等事件,在事件的捕获(capture)和冒泡(bubble)阶段都可以进行委托
比如我们可以加上对应的onClick和onClickCapture
function App() {
function onClickCapture() {
console.log('我是捕获阶段')
}
function onClick() {
console.log('我是冒泡阶段')
}
return <div onClickCapture={onClickCapture} onClick={onClick}>app</div>
}
而对于puase、play、scroll、load 等事件则只能在捕获(capture)阶段进行委托
这里涉及到事件合成机制的实现,实现过程比较复杂,鉴于本文主要讲createRoot的过程,就不细讲了,但这里我们要清楚的有3点:
- 😯,原来React的所有事件都是绑定到
#root上的,而不是绑定到诸如<div onClick={onClickFn}/>对应的dom上 - 😯,原来是一开始调用
ReactDOM.createRoot就初始化绑定了所有的事件,而不是我们在dom上写上对应的事件函数才委托到#root - 😯,原来对于支持
捕获和冒泡委托的事件,都会加上两个阶段的事件监听
总结
从上面的分析我们知道了ReactDOM.createRoot 做了什么:
-
createRoot的主要实现集中在createRootImpl -
createRootImpl中创建了fiberRoot和rootFiber(注意两者区别,不要混淆),前者的current指向后者,后者的stateNode指向前者 -
对于
dom对应的Fiber,除了rootFiber,大多有containerInfo指向真实的dom,而dom的'__reactContainer$' + randomKey属性上又指向了其对应的Fiber -
从一开始调用了
createRoot就在div#root初始化绑定了所有的监听事件,而不是在组件上写上对应的事件才绑定监听事件。对于支持捕获和冒泡阶段的事件,都会绑定两个阶段的事件,捕获事件就是在普通事件名后加上Capture后缀
万事开头难,我们已经完整解析了ReactDOM.createRoot(root).render(<App />);前面的ReactDOM.createRoot(root),那么之后的自然就进入的render(<App />)。里面涉及jsx的处理、hook的实现、时间片、beginWork、diff、completeWork、commit、调度机制、各种优先级、合成事件等等,我们会在接下来一一解析
最后
感谢留下足迹,如果您觉得文章不错😄😄,还请动动手指😋😋,点赞+收藏+转发🌹🌹