关于react.js:翻译翻译什么叫ReactDOMcreateRoot

3次阅读

共计 8300 个字符,预计需要花费 21 分钟才能阅读完成。

前言

  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 为 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)阶段都能够进行委托

比方咱们能够加上对应的 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、调度机制、各种优先级、合成事件等等,咱们会在接下来一一解析

最初

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

正文完
 0