前言

  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

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 创立一个并发模式的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#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(带有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;

两者的互相指向如图

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

!留神,下面的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生成),即

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

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

比方咱们能够加上对应的onClickonClickCapture

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

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

这里波及到事件合成机制的实现,实现过程比较复杂,鉴于本文次要讲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、调度机制、各种优先级、合成事件等等,咱们会在接下来一一解析

最初

感激留下脚印,如果您感觉文章不错,还请动动手指,点赞+珍藏+转发