React18.2x源码解析(一)react应用加载

前言

很少写前言,这次想说两句。之前一直主要是用的Vue技术栈,React使用相对较少。因为公司效益不好,已经处于行业下坡路,三四月份便有看外面机会的想法,刷过几次Boss,看到不少大的公司都是react技术栈,新资更好,同水平react的岗位就是比vue的待遇要更好一点,这是实话。于是就想趁这个机会再完整的学习一遍react技术栈,后面再考虑更好的机会,想好了就开始干。

五月份就完整的过了一遍react全家桶,其实说句实话,react和vue作为最流行的前端框架,只要你熟悉其中一个框架,上手另外一个也是非常快的。然后六月份就开始看react源码了,这段时间的进度非常缓慢,不仅是因为时间比较有限,平时基本是利用空闲时间和周末来学习,更多的还是因为React源码比较复杂,代码量也非常庞大,之前也看过Vue2Vue3的源码,但是难度方面确实也是显而易见的。

后面也是经常看网上一些资料文章进行参考学习,期间也正好是看到了卡颂的《React设计原理》,就买了这本书辅助学习,这本书对我来说最大的帮助就是梳理了react源码整体的架构和源码中一些抽象概念的理解,所以想要学习react源码的也推荐参考这本书帮助学习,当然最重要的还是要自己去看源码,去调试实操,不然收获是比较有限的。

个人喜欢在学习新的内容之后书写markdown文档进行总结,大大小小也已经积累了几十个文档了。所以这类文章算是我学习源码之后的一个笔记总结,当然还是进行了一定的优化,也绘制了一些相关的图例。因为水平有限可能存在部分理解有误,也希望理解和指出以便修正。本系列预计会有十篇左右,目前已经完成了七篇,后面可能每一两天更新一篇;第一到第四章节是讲解一个react应用加载的核心流程,所以一些细节会有所省略,比如组件详细的加载过程和hooks的处理等,这些内容会放在新的章节里面讲解。

源码解析的文章其实书写起来麻烦而且阅读体验可能也并不友好,主要是内容太多且枯燥,只能尽力而为,补充相关的调试截图以及案例配图,能对正在学习react源码的有一点帮助即可。

1,源码结构

React18.2的目录结构如下:

bash 复制代码
# 根目录
├── fixtures        # 一些React测试项目
├── packages        # react框架的各种源码包,比如react/react-dom/react-server等
├── scripts         # 各种工具的脚本文件,比如babel/devtools/eslint等

我们主要关注packages目录,里面是react框架的各种核心源码包:

bash 复制代码
├── react           # 通用的react框架核心API; 比如Component、createRef、useState、useRef等
├── react-dom       # dom平台相关的源码
├── react-art       # 处理canvas,svg等
├── react-server    # ssr服务器渲染相关的源码
├── react-native-renderer   # native环境
├── react-reconciler        # Fiber架构源码
├── scheduler       # 调度器源码
├── shared          # 整个项目通用的工具包,包含各种utils公用方法
...

2,应用入口

根据React18版本,每一个React应用的入口都是从react-dom这个源码包中引入一个ReactDOM对象,然后调用createRoot方法进行一系列的初始化准备,这个方法会返回一个root对象【ReactDOMRoot构造函数的实例】,最后调用root.render()方法开始一个React应用的加载渲染。

本源码系列都以浏览器端为视角,展开对React源码的学习。

JavaScript 复制代码
import React from 'react';
import ReactDOM from 'react-dom/client';
​
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
createRoot

接下来就从createRoot这个方法开始React源码的学习。

首先查看createRoot的源码:

JavaScript 复制代码
// packages\react-dom\src\client\ReactDOM.js
​
function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions,
): RootType {
      
  return createRootImpl(container, options);
}

这里createRootImpl方法为ReactDOMRoot.js文件中的createRoot方法。

继续深入源码:

JavaScript 复制代码
// packages\react-dom\src\client\ReactDOMRoot.js
​
export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions,
): RootType {
​
  # container: 默认是#root dom元素
  // 校验container是否为合法的dom元素
  if (!isValidContainer(container)) {
    throw new Error('createRoot(...): Target container is not a DOM element.');
  }
​
  // 开发环境校验警告
  warnIfReactDOMContainerInDEV(container);
​
  // 默认严格模式为false
  let isStrictMode = false;
  let concurrentUpdatesByDefaultOverride = false;
  let identifierPrefix = '';
  let onRecoverableError = defaultOnRecoverableError;
  let transitionCallbacks = null;
  ...
​
  # 创建root应用根节点对象,为FiberRootNode类型: 
  // 注:ConcurrentRoot为1,代表了v18版本的并发渲染模式
  const root = createContainer(
    container,
    ConcurrentRoot, // 1 默认开启并发模式
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
  );
​
  // 给#root DOM元素设置一个内部属性,存储root.current 即HostFiber对象
  markContainerAsRoot(root.current, container);
​
  const rootContainerElement: Document | Element | DocumentFragment =
    container.nodeType === COMMENT_NODE
      ? (container.parentNode: any)
      : container;
​
  // 监听[#root dom元素]的所有事件
  listenToAllSupportedEvents(rootContainerElement);
​
  # 创建ReactDOMRoot实例对象并返回 将root应用根节点对象 存储为ReactDOM的内部对象
  return new ReactDOMRoot(root);
}

createRoot方法首先对传入的container#root】容器元素进行合法校验:

  • 校验失败,抛出异常,停止应用加载。
  • 校验成功,继续向下执行应用加载。

然后初始化了一些默认的状态,这个有一个变量需要注意:

JavaScript 复制代码
let isStrictMode = false;

react18不会默认开启严格模式,需要自己使用<StrictMode> 组件包裹App根组件,才能开启整个应用的严格模式。

JavaScript 复制代码
<StrictMode>
  <App />
</StrictMode>

但是React18会默认开启并发渲染模式

JavaScript 复制代码
// 历史遗留模式,针对16,17版本
LegacyRoot = 0;
// 并发模式【react18默认开启】
ConcurrentRoot = 1;

createRoot方法中主要有三个重点逻辑处理:

  • 调用createContainer方法,创建root应用根节点对象。
  • #rootDOM元素上监听所有事件,这是React事件系统的关键。
  • 创建一个ReactDOMRoot实例对象并返回,这就是createRoot方法最后返回的root对象。

下面我们开始逐个解析每个逻辑过程。

createContainer

首先进入createContainer源码:

JavaScript 复制代码
// packages\react-reconciler\src\ReactFiberReconciler.new.js
​
export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
  identifierPrefix: string,
  onRecoverableError: (error: mixed) => void,
  transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
  // hydrate代表ssr,默认为false
  const hydrate = false;
  const initialChildren = null;
  // 只调用了一个方法
  return createFiberRoot(
    containerInfo,
    tag,
    hydrate,
    initialChildren,
    hydrationCallbacks,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
  );
}

继续查看createFiberRoot源码:

JavaScript 复制代码
// packages\react-reconciler\src\ReactFiberRoot.new.js
​
// 创建root应用根节点对象
export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
  ...
): FiberRoot {
​
  # 创建root应用根节点对象 【FiberRootNode】
  const root: FiberRoot = new FiberRootNode(
    containerInfo,
    tag, // 1
    hydrate,
    identifierPrefix,
    onRecoverableError,
  );
​
  # 创建一个FiberNode对象【HostRootFiber】,它是Fiber树的根节点,非常重要
  const uninitializedFiber = createHostRootFiber(
    tag,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
  );
    
  // 【关联起来,以便在后续的渲染过程中能够正确地处理该组件树的更新和重新渲染。】
  // 将root应用根节点对象的current属性 指向了当前Current Fiber Tree组件树的根节点【HostRootFiber】
  root.current = uninitializedFiber;
  // 然后将HostFiber.stateNode属性值:设置为root应用根节点对象
  uninitializedFiber.stateNode = root;
​
  if (enableCache) {
    const initialCache = createCache();
    retainCache(initialCache);
​
    root.pooledCache = initialCache;
    retainCache(initialCache);
    const initialState: RootState = {
      element: initialChildren,
      isDehydrated: hydrate,
      cache: initialCache,
      transitions: null,
      pendingSuspenseBoundaries: null,
    };
    uninitializedFiber.memoizedState = initialState;
  } else {
    const initialState: RootState = {
      element: initialChildren,
      isDehydrated: hydrate,
      cache: (null: any), // not enabled yet
      transitions: null,
      pendingSuspenseBoundaries: null,
    };
    // 初始化数据
    uninitializedFiber.memoizedState = initialState;
  }
​
  // 初始化HostRootFiber根节点对象的updateQueue属性
  initializeUpdateQueue(uninitializedFiber);
​
  # 返回root应用根节点对象【容器】
  return root;
}

createFiberRoot方法一进来就创建了一个root实例对象,它的类型为FiberRootNode

它是根据#root容器元素创建的对象,可以称为应用根节点

JavaScript 复制代码
const root = new FiberRootNode()

我们要理解这个对象的内容,就要查看FiberRootNode构造函数:

JavaScript 复制代码
function FiberRootNode(
  containerInfo,
  tag,
  hydrate,
  identifierPrefix,
  onRecoverableError,
) {
​
  this.tag = tag; // 应用模式:1 表示并发渲染模式
  this.containerInfo = containerInfo; // 存储#root dom对象
  this.pendingChildren = null;
  
  this.current = null; # 这个属性指向Current Fiber Tree的根节点 也就是HostFiberRoot
  this.pingCache = null;
  this.finishedWork = null; // 存储创建完成的FiberTree
  this.timeoutHandle = noTimeout;
  this.context = null;
  this.pendingContext = null;
  this.callbackNode = null; // 回调节点,存储当前任务task
  this.callbackPriority = NoLane; // 回调任务优先级
  this.eventTimes = createLaneMap(NoLanes);
  this.expirationTimes = createLaneMap(NoTimestamp);
​
  this.pendingLanes = NoLanes; // 默认 0
  this.suspendedLanes = NoLanes;
  this.pingedLanes = NoLanes;
  this.expiredLanes = NoLanes;
  this.mutableReadLanes = NoLanes;
  this.finishedLanes = NoLanes;
​
  this.entangledLanes = NoLanes;
  this.entanglements = createLaneMap(NoLanes);
  ...
}

下图示例:为debug调试中创建的FiberRootNode实例对象:

FiberRootNode中的实例属性很多,我们只需要先了解几个常用的。

下面我们继续回到createFiberRoot方法,然后又创建了一个Fiber对象:

JavaScript 复制代码
// hostFiber
const uninitializedFiber = createHostRootFiber()
JavaScript 复制代码
// packages\react-reconciler\src\ReactFiber.new.js
​
export function createHostRootFiber(
  tag: RootTag, // 1
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
): Fiber {
  let mode;
  ... 省略
​
  # 创建Fiber对象 tag为3即代表HostRoot节点 
  return createFiber(HostRoot, null, null, mode);
}

查看createFiber方法,这个内部直接调用FiberNode构造函数,创建了一个Fiber对象:

JavaScript 复制代码
const createFiber = function(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
): Fiber {
​
  // 创建FiberNode实例对象
  return new FiberNode(tag, pendingProps, key, mode);
};

继续查看FiberNode构造函数:

JavaScript 复制代码
function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
​
  this.tag = tag; // 节点类型,非常重要,不同的值代表不同的节点对象
  this.key = key;
  
  this.elementType = null; // 大部分情况同type,存储组件对象Element
  this.type = null; # 组件原始定义
  // 存储FiberNode对象对应的dom元素【hostCompoent】,
  // 函数组件此属性无值,
  // 类组件此属性存储的是组件实例instance
  this.stateNode = null; 
​
  # FiberNode节点之间的链接
  this.return = null; // 指向父级节点对象FiberNode
  this.child = null; // 指向第一个子节点FiberNode
  this.sibling = null; // 指向下一个兄弟节点FiberNode
  this.index = 0;
​
  this.ref = null; // ref引用
  
  # hooks相关
  this.pendingProps = pendingProps; // 新的,等待处理的props
  this.memoizedProps = null; // 旧的,上一次存储的props
  this.updateQueue = null; // 存储update更新对象链表
  this.memoizedState = null; // 类组件:旧的,上一次存储的state; 函数组件:存储hook链表
  this.dependencies = null;
​
  this.mode = mode; // 模式,作用?
​
  // 各种effect副作用相关的执行标记
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags; // 子孙节点的副作用标记,默认无副作用
  this.deletions = null; // 删除标记
​
  // 优先级调度,默认为0
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
​
  # 这个属性指向另外一个缓冲区对应的FiberNode
  // current.alternate === workInProgress
  // workInProgress.alternate === current
  this.alternate = null;
  ...
  
}

可以看到FiberNode同样定义了很多的实例属性,讲到这里必须要对FiberNode进行一些说明:

Fiber是VDOM是在React源码中的实现,FiberNode即虚拟dom,在Vue源码中定义为VNode

在React源码中有两种Fiber节点定义:FiberRootNodeFiberNode,但实际上我们说的Fiber节点是指的普通的FiberNode,我们从上面两个构造函数的定义可以发现,它们的实例属性完全不同,因为它们本来就是不一样的作用。

  • FiberRootNode:是针对React应用挂载的容器元素【#root】创建的一个对象,它是应用根节点。它的作用是负责应用加载相关的内容【应用级】,比如应用加载模式mode,存储本次应用更新的回调任务以及优先级,存储创建完成的FiberTree等。
  • FiberNode:这才是针对普通DOM元素或者组件创建的Fiber对象,是虚拟DOM的真实体现,它的属性存储了元素的或者组件的类型,对应的DOM信息,以及数据State和组件更新的相关信息。所以FiberNode才是Fiber架构的核心,而FiberRootNode主要是命名上的统一。

回到createContainer方法内:

JavaScript 复制代码
const uninitializedFiber = createHostRootFiber()

这里要注意创建的对象为HostRootFiber,虽然它也是普通的FiberNode类型,但是它的tag属性为3,代表着它是虚拟DOM树的根节点

JavaScript 复制代码
export const HostRoot = 3;

然后将FiberRootNodeHostRootFiber进行关联,方便在后续使用中可以互相访问:

JavaScript 复制代码
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;

以一张图表示它们的关系:

FiberRootNode是React应用的根节点【容器】,HostRootFiber是虚拟DOM树的根节点【FiberTree】。

通过属性关联之后,它们就可以在需要的时候互相访问。

回到createContainer方法内:

JavaScript 复制代码
function createContainer() {
​
  ...省略
    
  // 初始化HostFiber节点的updateQueue属性
  initializeUpdateQueue(uninitializedFiber);
​
  // 返回root应用根节点对象
  return root;
}

再返回root对象之前,还需要对HostRootFiber进行处理:

JavaScript 复制代码
# 初始化一个Fiber对象的updateQueue属性
export function initializeUpdateQueue<State>(fiber: Fiber): void {
  # 创建一个更新队列对象
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState, // 初始state数据
    firstBaseUpdate: null, 
    lastBaseUpdate: null,
    shared: {
      pending: null,
      lanes: NoLanes,
    },
    effects: null,
  };
​
  // 设置更新队列对象
  fiber.updateQueue = queue;
}

初始化HostRootFiber节点的updateQueue属性,为一个更新队列对象。

总结createContainer方法:

  • 创建FiberRootNode应用根节点对象。
  • 创建HostRootFiber虚拟DOM树根节点对象。
  • 关联两个对象,可以互相访问。
  • 初始化HostRootFiberupdateQueue属性。
  • 返回FiberRootNode节点。

事件监听

回到createRoot方法,在创建FiberRootNode应用根节点对象后,然后给#root根元素绑定了所有事件 ,任何子孙元素触发的该类型事件都会委托给【根元素的事件回调】处理。

JavaScript 复制代码
listenToAllSupportedEvents(rootContainerElement);

这里涉及到了React的合成事件系统,它的实现非常复杂,后面如果有时间会考虑单开一个章节详细解析,这里暂时只简单介绍一下它的基本原理,想了解更多的朋友也可以查阅相关资料学习。

  • #root根元素上绑定所有的事件回调,任何子孙元素触发的改类型事件都会委托给【根元素的事件回调】处理。
  • 寻找触发事件的dom元素,找到对应的FiberNode节点。
  • 收集从当前FiberNode节点到HostFiber之间的所有注册该事件的回调函数。
  • 循环执行一次收集的所有回调函数。

回到createRoot方法最后,创建了一个ReactDOMRoot实例对象并返回。

JavaScript 复制代码
return new ReactDOMRoot(root);

查看ReactDOMRoot构造函数的定义:

JavaScript 复制代码
function ReactDOMRoot(internalRoot: FiberRoot) {
  // 存储应用根节点[FiberRootNode]
  this._internalRoot = internalRoot;
}
​
// 原型方法
ReactDOMRoot.prototype.render = function() {}
ReactDOMRoot.prototype.unmount = function() {}

ReactDOMRoot构造函数只定义了一个实例属性_internalRoot,它的作用是存储创建完成的应用根节点FiberRootNode对象。

还在原型对象上定义了两个静态方法renderunmount

JavaScript 复制代码
import React from 'react';
import ReactDOM from 'react-dom/client';
​
const root = ReactDOM.createRoot(document.getElementById('root'));
// 调用render方法,开始应用加载
root.render(<App />);

到这里我们就可以对createRoot方法做一个总结: 它的主要作用就是为react应用加载做准备工作,初始化了一些根对象和基础信息,最后返回了一个ReactDOMRoot对象,调用它的render方法开启React应用的加载流程。

3,应用加载

最后我们再来看一下这个render方法:

JavaScript 复制代码
ReactDOMRoot.prototype.render = function(children) {
  const root = this._internalRoot;
  if (root === null) {
    throw new Error('Cannot update an unmounted root.');
  }
​
  // 开始加载react应用 children为App根组件, root为应用根节点对象FiberRootNode
  /**
   * react组件都会被转换为react-element对象:
   * {
   *    $$typeof: Symbol(react.element),
   *    key: null,
   *    props: {},
   *    ref: null,
   *    type: fun App() {}
   * }
   * 
   */
  updateContainer(children, root, null, null);
}
updateContainer

继续查看updateContainer源码:

JavaScript 复制代码
// packages\react-reconciler\src\ReactFiberReconciler.new.js
​
# 加载应用:触发调度更新任务
export function updateContainer(
  element: ReactNodeList, // App根组件
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
​
  // 取出current对象,为HostFiber 【它是当前Fiber树的根节点】
  const current = container.current;
  // 获取当前的程序执行时间【默认是performenct.now返回的微秒时间】
  const eventTime = requestEventTime();
    
  # 获取更新优先级lane
  const lane = requestUpdateLane(current);
​
  // 获取父组件的上下文,因为parentComponent为null,所以这里context为空对象
  const context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    // 从null变为{}
    // 如果容器的上下文为null, 则把父级上下文赋值
    container.context = context;
  } else {
    // 如果容器存在上下文,则把父级的上下文设置为等待处理的上下文
    container.pendingContext = context;
  }
​
  # 创建一个update更新对象 
  const update = createUpdate(eventTime, lane);
  // 将更新对象的 payload 属性设置为App根组件的内容,【即:本次应用加载的内容为App根组件】
  update.payload = {element};
​
  // 本来就没有,设置完还是null
  callback = callback === undefined ? null : callback;
  // 检查回调函数 callback 是否为空,如果不为空,则将其添加到更新对象的 callback 属性中
  if (callback !== null) {
    update.callback = callback;
  }
​
  # 将更新对象update:添加到当前current对象的更新队列中
  const root = enqueueUpdate(current, update, lane);
  if (root !== null) {
    # 开启一个新的调度更新任务
    scheduleUpdateOnFiber(root, current, lane, eventTime);
    entangleTransitions(root, current, lane);
  }
​
  return lane;
}

updateContainer方法的内容非常重要,主要是比较典型 。你可以在react源码中的很多地方看到类似的执行逻辑,react应用的加载会执行这些逻辑,一个state状态的变化,触发的更新也会执行这些逻辑。我们可以把它的执行过程分为四个重点部分:

  • 获取更新优先级lane
  • 创建update更新对象 。
  • update更新对象添加到目标Fiber对象的更新队列中。
  • 开启一个新的调度更新任务。

接下来我们逐个分析执行过程:

(一)获取更新优先级lane

JavaScript 复制代码
const lane = requestUpdateLane(current); // HostFiber

查看requestUpdateLane方法:

JavaScript 复制代码
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
​
# 获取更新优先级lane
export function requestUpdateLane(fiber: Fiber): Lane {
​
  const mode = fiber.mode;
  if ((mode & ConcurrentMode) === NoMode) {
    // 1,模式为0时,返回同步优先级
    return (SyncLane: Lane);
  } else if (
    !deferRenderPhaseUpdateToNextBatch &&
    (executionContext & RenderContext) !== NoContext &&
    workInProgressRootRenderLanes !== NoLanes
  ) {
    // 2,render阶段产生的update【比如调用fun()组件的过程中又触发了更新】,返回render阶段进行中的优先级
    return pickArbitraryLane(workInProgressRootRenderLanes);
  }
​
  const isTransition = requestCurrentTransition() !== NoTransition;
  // 3,Transition相关优先级:如果当前不是渲染上下文,并且请求的更新是过渡(transition),则进入下一个条件。
  if (isTransition) {
      transition._updatedFibers.add(fiber);
    }
    if (currentEventTransitionLane === NoLane) {
      currentEventTransitionLane = claimNextTransitionLane();
    }
    return currentEventTransitionLane;
  }
​
  // 4,使用手动设置的优先级:获取当前更新通道优先级 :默认为0
  const updateLane: Lane = (getCurrentUpdatePriority(): any);
  // NoLane 0
  if (updateLane !== NoLane) {
    // 只要更新通道不等于0,则返回有效的更新通道
    return updateLane;
  }
  // 5,获取当前事件级的优先级: 默认为16
  const eventLane: Lane = (getCurrentEventPriority(): any);
  return eventLane;
​
}

在react中,为了实现并发渲染和优先级控制,使用了多个更新通道(lane)来管理更新任务。每个更新通道都对应一个优先级,根据不同的优先级,react可以决定哪些更新任务可以同时进行,哪些需要等待之前的更新任务完成后才能进行。

requestUpdateLane函数的作用是:接收一个Fiber对象作为参数,并返回一个新的更新通道【更新优先级】,这个优先级lane会根据当前Fiber对象的相关属性或者当前的更新环境确定的。

requestUpdateLane方法内有以上五种情况对应的优先级lane。在应用首次加载时不满足前面的条件,最后就会返回eventLane,作为本次更新的优先级lane

JavaScript 复制代码
const eventLane = getCurrentEventPriority()
JavaScript 复制代码
export function getCurrentEventPriority() {
  const currentEvent = window.event;  // 应用首次加载,这里为undefined
  if (currentEvent === undefined) {
    # 返回默认事件优先级16
    return DefaultEventPriority;
  }
  // 返回事件对应的优先级
  return getEventPriority(currentEvent.type);
}

因为react应用首次加载并不是由某个事件触发,所以这里就会返回默认的事件优先级lane来作为应用初始加载的优先级。

优先级体系

在react源码中,Scheduler调度器是一个独立的包,React与Scheduler没有共用一套优先级体系:

react中有四种优先级,称为事件优先级,Scheduler有五种优先级,称为调度优先级。

  • react的事件优先级:
JavaScript 复制代码
// 1,对应离散事件的优先级,就是普通的事件比如click,input,blur等,这些事件对应的是同步优先级
export const DiscreteEventPriority: EventPriority = SyncLane;
// 2,对应的连续事件的优先级,比如drag,mousemove,scroll,wheel等,
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
// 3,默认的优先级
export const DefaultEventPriority: EventPriority = DefaultLane;
// 4,空闲情况的优先级,最低
export const IdleEventPriority: EventPriority = IdleLane;
  • scheduler的调度优先级:
JavaScript 复制代码
# 值越小优先级越高
export const ImmediatePriority = 1; // 最高优先级,同步执行
export const UserBlockingPriority = 2;
export const NormalPriority = 3; // 默认优先级
export const LowPriority = 4;
export const IdlePriority = 5; // 最低优先级

React与Scheduler的优先级转换需要:通过lane转化。

  • ReactScheduler的优先级转换:
JavaScript 复制代码
const eventLane = lanesToEventPriority(nextLanes)
​
let schedulerPriorityLevel; 
# lanes转换成事件优先级,匹配符合的事件优先级,然后赋值对应的scheduler的优先级
switch (eventLane) {
  // 同步优先级
  case DiscreteEventPriority:
    schedulerPriorityLevel = ImmediateSchedulerPriority;
    break;
  // 连续事件优先级
  case ContinuousEventPriority:
    schedulerPriorityLevel = UserBlockingSchedulerPriority;
    break;
  // 默认事件优先级
  case DefaultEventPriority:
    schedulerPriorityLevel = NormalSchedulerPriority;
    break;
  // 空闲事件优先级
  case IdleEventPriority:
    schedulerPriorityLevel = IdleSchedulerPriority;
    break;
  // 默认
  default:
    schedulerPriorityLevel = NormalSchedulerPriority;
    break;
}
  • SchedulerReact的优先级转换:
JavaScript 复制代码
// 获取当前调度的优先级,然后根据优先级匹配到对应的优先级,最后返回对应的事件优先级
const schedulerPriority = getCurrentSchedulerPriorityLevel();
switch (schedulerPriority) {
  case ImmediateSchedulerPriority:
    return DiscreteEventPriority;
        
  case UserBlockingSchedulerPriority:
    return ContinuousEventPriority;
        
  case NormalSchedulerPriority:
  case LowSchedulerPriority:
    return DefaultEventPriority;
        
  case IdleSchedulerPriority:
    return IdleEventPriority;
        
  default:
    return DefaultEventPriority;
}
react中的位运算

这里还有一个注意点:

JavaScript 复制代码
export function requestUpdateLane(fiber: Fiber): Lane {
​
  const mode = fiber.mode;
  // 位运算
  if ((mode & ConcurrentMode) === NoMode) {
    return (SyncLane: Lane);
  } 
}
JavaScript 复制代码
mode & ConcurrentMode

这里算是我们在react源码中遇到的第一个位运算处理,所以需要提前说明一下。如果你阅读过react源码,就可以发现其内部使用了大量的位运算操作,其中最典型的就是lanes优先级和flags副作用标记。

这里只介绍三种基本的位运算,因为react源码中主要使用的就是三种,关于位运算的更多信息可以参考《JavaScript高级程序设计4》中的位操作符章节。

  • 按位与&
第一个数值的位 第二个数值的位 结 果
1 1 1
1 0 0
0 1 0
0 0 0

按位与操作在两个位都是 1 时返回 1,在任何一位是 0 时返回 0

这里直接以上面的代码为例:此时mode3ConcurrentMode1

复制代码
3 & 1

计算3&1,首先将操作数转化为Int32

JavaScript 复制代码
// 3
0b00 0000 0000 0000 0000 0000 0000 0011
// 1
0b00 0000 0000 0000 0000 0000 0000 0001
// &计算
0b00 0000 0000 0000 0000 0000 0000 0001 // // 结果1

3&1计算结果为1,不等于NoMode【0】,所以不满足条件。

  • 按位或|
第一个数值的位 第二个数值的位 结 果
1 1 1
1 0 1
0 1 1
0 0 0

按位或操作在至少一位是 1 时返回 1,两位都是 0 时返回 0

计算3 | 1

JavaScript 复制代码
// 3
0b00 0000 0000 0000 0000 0000 0000 0011
// 1
0b00 0000 0000 0000 0000 0000 0000 0001
// 3|1计算
0b00 0000 0000 0000 0000 0000 0000 0011 // 结果为3
  • 按位非~
一个操作数的bit位:取反 结果
1 0
0 1

按位或操作是对一个操作数进行取反操作【0,1互换】。

计算~3

JavaScript 复制代码
// 3
0b00 0000 0000 0000 0000 0000 0000 0011
// ~3 逐位取反
0b11 1111 1111 1111 1111 1111 1111 1100 // 结果为-4

(二)创建update更新对象

JavaScript 复制代码
const update = createUpdate(eventTime, lane);
JavaScript 复制代码
// packages\react-reconciler\src\ReactFiberClassUpdateQueue.new.js
​
# 创建更新对象
export function createUpdate(eventTime: number, lane: Lane): Update<*> {
  // 定义update对象
  const update: Update<*> = {
    eventTime,
    lane, // 更新优先级
    tag: UpdateState,  // 不同的tag对应不同的更新场景
    payload: null, // 更新的内容   == fun组件用action字段
    callback: null, // 回调函数
    next: null, // 链表形式:指向下一个更新的对象【保证了update之间的顺序】
  };
​
  // 返回创建的update对象
  return update;
}

update对象的存储了更新相关的一些基本信息。

  • tag属性:不同值对应着不同的更新场景:
JavaScript 复制代码
// 1,默认情况:通过ReactDOM.createRoot或者this.setState触发
export const UpdateState = 0;
// 2,在classCompont组件生命周期函数中使用this.setState触发更新
export const ReplaceState = 1;
// 3,通过this.forceUpdate触发
export const ForceUpdate = 2;
// 4,发生错误的情况下在classComponent或者HostRoot中触发更新
export const CaptureUpdate = 3;

当前是通过ReactDOM.createRoot触发的应用加载,所以update对象的tag属性值为0

  • payload属性值对应的情况:
JavaScript 复制代码
# 1,ReactDOM.reacteRoot
{
    payload: { element } // 即<App />组件对应的element对象
}
​
# 2,classComponent组件
// this.setState({ count: 1 })
{
    payload: { count: 1 } // 也有可能是一个函数
}

当前的payload属性存储的是App根组件对应的element对象。

  • callback属性:当前为null。如果是this.setState触发的更新,则为此方法传入的第二个参数【回调函数】。
  • next属性:指向下一个update更新对象,形成一个单向环状链表。

总结: 根据上面的tag值,我们可以看出类组件的更新与ReactDOM.createRoot共用同一个update更新对象结构,对于ReactDOM.createRoot来说,当前创建的这个update对象,它的作用主要在于在react应用的首次加载【提供的初始信息】。

(三)将update更新对象添加到目标Fiber对象的更新队列中

JavaScript 复制代码
const root = enqueueUpdate(current, update, lane);
JavaScript 复制代码
// packages\react-reconciler\src\ReactFiberClassUpdateQueue.new.js
​
export function enqueueUpdate<State>(
  fiber: Fiber, // 要更新的节点 ,当前也就是hostFiber
  update: Update<State>, // 更新对象
  lane: Lane, // 更新的优先级
): FiberRoot | null {
​
  # 取出当前Fiber节点的updateQueue属性
  const updateQueue = fiber.updateQueue;
  if (updateQueue === null) {
    // Only occurs if the fiber has been unmounted.
    // 表明当前节点已经被卸载
    return null;
  }
​
  // 一个Fiber节点会先初始化updateQueue属性,后创建Update对象
​
  // 取出updateQueue.shared对象 { lanes: 0, pending: null }
  const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
​
  // 判断当前是否为不安全的渲染阶段 更新
  if (isUnsafeClassRenderPhaseUpdate(fiber)) {
    const pending = sharedQueue.pending;
    if (pending === null) {
      update.next = update;
    } else {
      update.next = pending.next;
      pending.next = update;
    }
    sharedQueue.pending = update;
    return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
  } else {
    # 默认是处于安全的渲染阶段更新:讲update对象添加到queue之中
    return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
  }
}

前面我们已经知道了update对象是存储更新相关的一些基本信息,可以说它是一个基本的更新装置,而updateQueue很明显就是来保存或者说管理update对象的数据结构。

经过上面的处理,最终会将update更新对象存储到当前Fiber节点中,形成一个单向环状链表。这里描述的比较简单,因为对于react应用加载的场景,updateupdateQueue的作用并不明显。它们更重要的使用场景,在于组件更新时的逻辑处理。

关于类组件updateQueue这部分详细内容可以查看《React18.2x源码解析:类组件的加载过程》。

关于函数组件updateQueue这部分详细内容可以查看《React18.2x源码解析:函数组件的加载过程》。

(四)开启一个新的调度更新任务

JavaScript 复制代码
// 开启一个新的的调度更新任务
scheduleUpdateOnFiber(root, current, lane, eventTime);

scheduleUpdateOnFiber函数非常重要,在react源码中有很多地方在调用,它是react开始执行调度更新的入口函数。

从这个函数的名称我们也可以知道它的作用:开启一个新的调度更新任务。

下一章节我们将从这个函数开始进入scheduler调度程序的执行过程。

相关推荐
@大迁世界7 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路16 分钟前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug19 分钟前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213821 分钟前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中43 分钟前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
冰暮流星1 小时前
javascript逻辑运算符
开发语言·javascript·ecmascript
前端 贾公子1 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗1 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全