前言
- 本文源码版本是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、调度机制、各种优先级、合成事件等等,我们会在接下来一一解析
最后
感谢留下足迹,如果您觉得文章不错😄😄,还请动动手指😋😋,点赞+收藏+转发🌹🌹