前言
- 本文源码版本是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为1const containerNodeType = container.nodeType;// COMMENT_NODE = 8,代表正文,nodeType = 1, 代表元素// nodeType具体可看https://www.w3school.com.cn/jsref/prop_node_nodetype.aspconst 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、调度机制、各种优先级、合成事件等等,咱们会在接下来一一解析
最初
感激留下脚印,如果您感觉文章不错,还请动动手指,点赞+珍藏+转发