翻译翻译,什么叫ReactDOM.createRoot

4,556 阅读4分钟

前言

  1. 本文源码版本是React 17.0,源码分支为feature/createRoot,分析的模式是Concurrent(并发)模式,且对一些不太重要的源码进行了删除
  2. 要想搞清楚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则是一个介于LegacyRootConcurrentRoot之间的过渡模式,这里我们略过

那我们可以大概猜测到,实际上的主要实现应该都是在接下来的createRootImpl

createRootImpl

createRootImplimpl的全称为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 创建一个并发模式的fiberRootroot

 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(带有RootTagFiber),然后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;

两者的相互指向如图

image.png

之后构建出的Fiber树相互指向如下:

image.png

!注意,上面的fiberRootrootFiber可不是同个东西(笔者私以为React里面这样的命名很容易让初学者混淆🐶,所以这里特别强调一下),fiberRoot的类型是FiberRoot,通过new FiberRootNode生成,fiberRoot通过改变current指针指向来渲染不同的页面。而rootFiber是通过new FiberNode生成,与Appdiv等都有对应的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生成),即

{
   $$typeofSymbol(react.element)
   keynull
   props: {}
   refnull
   type: App()
   _ownernull
}

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;
}

image.png

因此,对于React的dom根节点,我们都可以在控制台通过获取div#root__reactContainer$randomKey 来获取对应的fiber(其他dom节点通过__reactFiber$randomKey获取),这里以知乎为例:

image.png

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一栏来看看上面绑定了什么事件监听

对于clickchange等事件,在事件的捕获(capture)和冒泡(bubble)阶段都可以进行委托

image.png

比如我们可以加上对应的onClickonClickCapture

function App() {

  function onClickCapture() {
    console.log('我是捕获阶段')
  }
  function onClick() {
    console.log('我是冒泡阶段')
  }
  return <div onClickCapture={onClickCapture} onClick={onClick}>app</div>
}

而对于puaseplayscrollload 等事件则只能在捕获(capture)阶段进行委托

image.png

这里涉及到事件合成机制的实现,实现过程比较复杂,鉴于本文主要讲createRoot的过程,就不细讲了,但这里我们要清楚的有3点:

  1. 😯,原来React的所有事件都是绑定到#root上的,而不是绑定到诸如<div onClick={onClickFn}/>对应的dom
  2. 😯,原来是一开始调用ReactDOM.createRoot就初始化绑定了所有的事件,而不是我们在dom上写上对应的事件函数才委托到#root
  3. 😯,原来对于支持捕获冒泡委托的事件,都会加上两个阶段的事件监听

总结

从上面的分析我们知道了ReactDOM.createRoot 做了什么:

  1. createRoot 的主要实现集中在 createRootImpl

  2. createRootImpl中创建了 fiberRootrootFiber(注意两者区别,不要混淆),前者的current指向后者,后者的stateNode 指向前者

  3. 对于dom 对应的Fiber,除了rootFiber,大多有 containerInfo 指向真实的dom,而dom'__reactContainer$' + randomKey 属性上又指向了其对应的 Fiber

  4. 从一开始调用了 createRoot 就在 div#root 初始化绑定了所有的监听事件,而不是在组件上写上对应的事件才绑定监听事件。对于支持捕获和冒泡阶段的事件,都会绑定两个阶段的事件,捕获事件就是在普通事件名后加上Capture后缀

万事开头难,我们已经完整解析了ReactDOM.createRoot(root).render(<App />);前面的ReactDOM.createRoot(root),那么之后的自然就进入的render(<App />)。里面涉及jsx的处理、hook的实现、时间片、beginWork、diff、completeWork、commit、调度机制、各种优先级、合成事件等等,我们会在接下来一一解析

最后

感谢留下足迹,如果您觉得文章不错😄😄,还请动动手指😋😋,点赞+收藏+转发🌹🌹