共计 8300 个字符,预计需要花费 21 分钟才能阅读完成。
前言
- 本文源码版本是 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#root
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、调度机制、各种优先级、合成事件等等,咱们会在接下来一一解析
最初
感激留下脚印,如果您感觉文章不错😄😄,还请动动手指😋😋,点赞 + 珍藏 + 转发🌹🌹