React源码解读之任务调度

news2025/7/18 8:18:33

React 设计体系如人类社会一般,拨动时间轮盘的那一刻,你便成了穿梭在轮片中的一粒细沙,角逐过程处处都需要亮出你的属性,你重要吗?你无可替代吗?你有特殊权限吗?没有,那不好意思,请继续在轮片中循环。属于你的生命之火殆尽,前来悼念之人很多,这幕,像极了出生时的场景。

干啥玩意儿,这是技术文章不是抒情散文!下面进入正题。

创建的准备上一节已经说明了,主要定义与更新相关的数据结构和变量,计算过期时间等。完成这些准备工作之后,正式进入调度工作,调度过程实现思路是:当与更新或挂载相关api被调用时,就会执行更新的逻辑,更新大致分为以下几个小阶段

scheduleWork

该步骤的主要工作有以下几点

  1. 通过 scheduleWorkOnParentPath 方法找到当前 Fiber 的root节点
  2. 遍历当前更新节点父节点上的每个节点,对比每个节点的 expirationTime ,如果大于当前节点,则将其值赋值为当前节点的 expirationTime 值。同时,childExpirationTime 的值也是该的逻辑
export function scheduleUpdateOnFiber(
  fiber: Fiber,
  expirationTime: ExpirationTime,
) {
  checkForNestedUpdates();
  warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);

  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return;
  }

  checkForInterruption(fiber, expirationTime);
  recordScheduleUpdate();

  // TODO: computeExpirationForFiber also reads the priority. Pass the
  // priority as an argument to that function and this one.
  const priorityLevel = getCurrentPriorityLevel();

  if (expirationTime === Sync) {
    if (
      // Check if we're inside unbatchedUpdates
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      // Check if we're not already rendering
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      // Register pending interactions on the root to avoid losing traced interaction data.
      schedulePendingInteractions(root, expirationTime);

      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root);
      schedulePendingInteractions(root, expirationTime);
      if (executionContext === NoContext) {
        flushSyncCallbackQueue();
      }
    }
  } else {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
  }

    ...
}
export const scheduleWork = scheduleUpdateOnFiber;

如果过期时间等于我们定义的Sync常量对应值,则进一步判断这次更新的状态,如果不是 batchUpdates 什么时候不是这个状态呢?我们前面认识过,比如reder时,判断完这个状态后还需要保证这次的更新渲染已准备好,则开始处理。不过处理之前,还要进行一个操作就是pending interaction,与我们动作相关的内容数据需要保存于 pendingInteractionMap 中。相关参考视频讲解:进入学习

function scheduleInteractions(root, expirationTime, interactions) {
  if (!enableSchedulerTracing) {
    return;
  }

  if (interactions.size > 0) {
    const pendingInteractionMap = root.pendingInteractionMap;
    const pendingInteractions = pendingInteractionMap.get(expirationTime);
    if (pendingInteractions != null) {
      interactions.forEach(interaction => {
        if (!pendingInteractions.has(interaction)) {
          // Update the pending async work count for previously unscheduled interaction.
          interaction.__count++;
        }

        pendingInteractions.add(interaction);
      });
    } else {
      pendingInteractionMap.set(expirationTime, new Set(interactions));

      // Update the pending async work count for the current interactions.
      interactions.forEach(interaction => {
        interaction.__count++;
      });
    }

    const subscriber = __subscriberRef.current;
    if (subscriber !== null) {
      const threadID = computeThreadID(root, expirationTime);
      subscriber.onWorkScheduled(interactions, threadID);
    }
  }
}

经过以上处理,就能进入 performSyncWorkOnRoot 处理了

function performSyncWorkOnRoot(root) {
  // Check if there's expired work on this root. Otherwise, render at Sync.
  const lastExpiredTime = root.lastExpiredTime;
  const expirationTime = lastExpiredTime !== NoWork ? lastExpiredTime : Sync;
  if (root.finishedExpirationTime === expirationTime) {
    commitRoot(root);
  }
  ...
}

好了,到这里一个expirationTimeSync 的且不是unbatchedUpdates,的调度就完成了,我们发现这条流水线的操作还是容易理解的,好,我们现在进入另一个分支,就是 batchedUpdates

ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
if (executionContext === NoContext) {
  // Flush the synchronous work now, unless we're already working or inside
  // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
  // scheduleCallbackForFiber to preserve the ability to schedule a callback
  // without immediately flushing it. We only do this for user-initiated
  // updates, to preserve historical behavior of legacy mode.
  flushSyncCallbackQueue();
}

首先需要确保一点,Root是否已经处理过调度相关工作,通过 ensureRootIsScheduled 方法为root创建调度任务,且一个root只有一个task,假如某个root已经存在了任务,换言之已经调度过,那么我们需要重新为这个task计算一些值。而后同样有一个 schedulePendingInteractions ,用来处理交互引起的更新,方式与上面提到的 pending interaction 类似。

另外,如果executionContextNoContext ,则需要刷新用于处理同步更新的回调队列 flushSyncCallbackQueue ,该方法定义在 SchedulerWithReactIntegration.js 中。

如此,周而复始,完成更新的调度过程,最终调用 performSyncWorkOnRoot ,进入下一阶段,

performSyncWorkOnRoot

同样的选择题,当前是否能直接去提交更新,yes or no ?

if (root.finishedExpirationTime === expirationTime) {
  // There's already a pending commit at this expiration time.
  // TODO: This is poorly factored. This case only exists for the
  // batch.commit() API.
  commitRoot(root);
}

这种情况是很少的,一般会进入这个判断的else,也就是

...
workLoopSync();
...

function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

又开始了遍历,这个遍历中同样有我们上节分析过一些技巧,比如unitOfWork.alternate 用于节点属性的对比与暂存

function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
  // The current, flushed, state of this fiber is the alternate. Ideally
  // nothing should rely on this, but relying on it here means that we don't
  // need an additional field on the work in progress.
  const current = unitOfWork.alternate;

  startWorkTimer(unitOfWork);
  setCurrentDebugFiberInDEV(unitOfWork);

  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, renderExpirationTime);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, renderExpirationTime);
  }

  resetCurrentDebugFiberInDEV();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(unitOfWork);
  }

  ReactCurrentOwner.current = null;
  return next;
}

可以看到执行完相关操作后,随着 beginWork 函数的调用正式进入更新阶段。

beginWork

该部分主要的工作就是更新,更新什么呢?我们第一节讲到 React 不同的组件使用?typeof 指定,针对这些不同类型的组件,定义了各自的处理方法,我们以常用的 ClassComponent 为例。

function beginWork(
  current: Fiber | null,  workInProgress: Fiber,  renderExpirationTime: ExpirationTime,
): Fiber | null {
  const updateExpirationTime = workInProgress.expirationTime;
  ...

而后首先判断当前的更新节点是否为空,若不为空,则执行相关逻辑

...
if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      // Force a re-render if the implementation changed due to hot reload:
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // If props or context changed, mark the fiber as having performed work.
      // This may be unset if the props are determined to be equal later (memo).
      didReceiveUpdate = true;
    } else if (updateExpirationTime < renderExpirationTime) {
      didReceiveUpdate = false;
      ...

此刻略知一二,前后props是否发生更改?根据不同的条件判断为 didReceiveUpdate 赋值。而后根据当前 workInProgress 的tag值判断当前的节点对应组件类型是什么,根据不同类型,进入不同方法进行处理。

switch (workInProgress.tag) {
    ...
}

而后,同样根据该tag,执行更新组件逻辑

case ClassComponent: {
  const Component = workInProgress.type;
  const unresolvedProps = workInProgress.pendingProps;
  const resolvedProps =
        workInProgress.elementType === Component
  ? unresolvedProps
  : resolveDefaultProps(Component, unresolvedProps);
  return updateClassComponent(
    current,
    workInProgress,
    Component,
    resolvedProps,
    renderExpirationTime,
  );
}

reconcileChildren

更新组件过程中,如果还有子节点,需要调度并更新

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderExpirationTime: ExpirationTime,
) {
  if (current === null) {
    // If this is a fresh new component that hasn't been rendered yet, we
    // won't update its child set by applying minimal side-effects. Instead,
    // we will add them all to the child before it gets rendered. That means
    // we can optimize this reconciliation pass by not tracking side-effects.
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderExpirationTime,
    );
  } else {
    // If the current child is the same as the work in progress, it means that
    // we haven't yet started any work on these children. Therefore, we use
    // the clone algorithm to create a copy of all the current children.

    // If we had any progressed work already, that is invalid at this point so
    // let's throw it out.
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderExpirationTime,
    );
  }
}

其子节点的 Fiber 调度定义在 ReactChildFiber.js 中,这里不展开了。

commitRoot

轮回中完成以上调度过程,也该到了提交更新的时候了,该方法我们在刚开始就讲到了,那时略过,现在拾起。

function commitRoot(root) {
  const renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority(
    ImmediatePriority,
    commitRootImpl.bind(null, root, renderPriorityLevel),
  );
  return null;
}

具体的实现在 commitRootImpl 方法中,该方法调用 prepareForCommit 为更新做准备,最终根据更新的类型不同使用不同策略进行更新

let primaryEffectTag =
      effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
    case Placement: {
    commitPlacement(nextEffect);
      // Clear the "placement" from effect tag so that we know that this is
      // inserted, before any life-cycles like componentDidMount gets called.
      // TODO: findDOMNode doesn't rely on this any more but isMounted does
      // and isMounted is deprecated anyway so we should be able to kill this.
      nextEffect.effectTag &= ~Placement;
      break;
    }
  case PlacementAndUpdate: {
    // Placement
    commitPlacement(nextEffect);
    // Clear the "placement" from effect tag so that we know that this is
    // inserted, before any life-cycles like componentDidMount gets called.
    nextEffect.effectTag &= ~Placement;

    // Update
    const current = nextEffect.alternate;
    commitWork(current, nextEffect);
    break;
  }
  case Hydrating: {
    nextEffect.effectTag &= ~Hydrating;
    break;
  }
  case HydratingAndUpdate: {
    nextEffect.effectTag &= ~Hydrating;

    // Update
    const current = nextEffect.alternate;
    commitWork(current, nextEffect);
    break;
  }
  case Update: {
    const current = nextEffect.alternate;
    commitWork(current, nextEffect);
    break;
  }
  case Deletion: {
    commitDeletion(root, nextEffect, renderPriorityLevel);
    break;
  }
}

提交更新相关的处理定义于 ReactFiberCommitWork.js 同样也要借助 tag,做不同策略的处理。

至此完成了任务调度的所有工作,当然在后面的过程,事件相关的处理是只字未提,React最新源码对于事件系统做了很大改动,我们放在后面章节详细讲解。React 源码设计之精妙无法言尽,并且只是略读,完成本系列的粗略讲解后,后续会有更深入源码讲解。读源码为了什么?

  1. 理解我们每天使用的框架工作原理
  2. 学习作者NB的设计和对于代码极致的追求,运用到自己的项目中

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/8290.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Autosar模块介绍:AutosarOS(5)

上一篇 | 返回主目录 | 下一篇 AutosarOS&#xff1a;错误处理、跟踪与调试&#xff08;5&#xff09;1 钩子例程2 错误处理&#xff08;ErrorHook&#xff09;3 系统启动&#xff08;StartupHook&#xff09;4 系统关闭&#xff08;ShutdownHook&#xff09;5 系统保护&#x…

【面试题】margin负值问题

margin-top和margin-left负值&#xff0c;元素向上、向左移动&#xff1b;margin-right负值&#xff0c;右侧元素左移&#xff0c;自身不受影响&#xff1b;margin-bottom负值&#xff0c;下方元素上移&#xff0c;自身不受影响&#xff1b; 1. margin top left为负数 <st…

0095 贪心算法,普利姆算法

import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; /* * 贪心算法 * 1.指在对问题进行求解时&#xff0c;在 每一步 选择中都采取最好或最优的选择&#xff0c;希望能够导致结果是最好或最优的算法 * 2.所得到的结果不一定是最优结果&…

【SSH远程登录长时间连接后容易出现自动断开的解决方案】

SSH远程登录长时间连接后容易出现自动断开的解决方案0 问题描述1 方法一1.1 打开ssh_config文件1.2 在文件中添加以下内容1.3 重启ssh2 方法二2.1 打开sshd_config文件2.2 在文件中添加以下内容2.3 重启ssh0 问题描述 使用SSH连接远程服务器的时候 报出 client_loop send disc…

时序分析 48 -- 时序数据转为空间数据 (七) 马尔可夫转换场 python 实践(下)

时序分析 48 – 时序数据转为空间数据 (七) 马尔可夫转换场 python 实践&#xff08;下&#xff09; … 接上 从MTF到图模型 从MTF中我们可以生成图 &#x1d43a;(&#x1d449;,&#x1d438;)&#x1d43a;(&#x1d449;,&#x1d438;)G(V,E) &#xff0c;节点V和时间…

Redis从理论到实战:使用Redis实现商铺查询缓存(逐步分析缓存更新策略)

文章目录一、什么是缓存二、缓存的作用三、添加商户缓存四、分析缓存更新策略1、删除缓存还是更新缓存&#xff1f;2、如何保证缓存与数据库的操作同时成功或失败&#xff1f;3、先操作缓存还是先操作数据库&#xff1f;加油加油&#xff0c;不要过度焦虑(#^.^#) 一、什么是缓存…

ThreadLocal为什么会出现内存泄漏,你真的知道吗?

目录 1 前言 2 ThreadLocal进行线程隔离的小示例 3 原因 1 前言 大家想要搞清楚这个问题&#xff0c;就必须知道内存泄漏和内存溢出的区别 内存泄漏&#xff1a;不就被使用的对象或者变量无法被回收 内存溢出&#xff1a;没有剩余的空间来创建新的对象 2 ThreadLocal进行…

Java中的字符串

&#x1f649; 作者简介&#xff1a; 全栈领域新星创作者 &#xff1b;天天被业务折腾得死去活来的同时依然保有对各项技术热忱的追求&#xff0c;把分享变成一种习惯&#xff0c;再小的帆也能远航。 &#x1f3e1; 个人主页&#xff1a;xiezhr的个人主页 java中的字符串一、简…

C++:重定义:符号重定义:变量重定义

概述&#xff1a;在上一篇我们知道 通过 #ifndef....#defin....#endif &#xff0c; 这个解决头文件重复包含的问题 C&#xff1a;重定义&#xff1a;class类型重定义_hongwen_yul的博客-CSDN博客 避免头文件的重复包含可以有效的避免变量的重复定义&#xff0c;其实不光变量…

[附源码]java毕业设计基于web旅游网站的设计与实现

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

使用Docker开发GO应用程序

根据Stack Overflow的2022开发者调查&#xff0c;Go&#xff08;或Golang&#xff09;是最受欢迎和最受欢迎的编程语言之一。由于与许多其他语言相比&#xff0c;Go的二进制大小更小&#xff0c;开发人员经常使用Go进行容器化应用程序开发。 Mohammad Quanit在他的社区全能课程…

小程序vant-tabbar使用示例,及报错处理

小程序vant-tabbar使用示例&#xff0c;及报错处理1. 配置信息2. 添加 tabBar 代码文件3. 编写 tabBar 代码custom-tab-bar/index.tscustom-tab-bar/index.jsoncustom-tab-bar/index.wxml使小程序使用vant-tabbar组件时&#xff0c;遇到以下报错&#xff1a;Couldn’t found th…

Java基于springboot+vue的儿童玩具销售购物网站 多商家

爱玩儿是所有孩子的天性。尤其是在婴幼儿阶段。选择一个好的玩具&#xff0c;不仅能够让孩子玩儿的开心&#xff0c;而且有助于孩子智力的开发。很多家长在选择玩具的时候&#xff0c;不知道选择什么样的玩具。且当前玩具市场的玩具鱼目混杂&#xff0c;种类繁多&#xff0c;而…

SAR信号处理基础1——线性调频信号

关键字&#xff1a;线性调频信号&#xff0c;LFM信号&#xff0c;chirp信号&#xff0c;驻定相位原理&#xff08;POSP&#xff09;&#xff0c;泰勒展开&#xff0c;Taylor展开&#xff0c;脉冲压缩&#xff0c;匹配滤波&#xff0c;sinc&#xff0c;分辨率&#xff0c;峰值旁…

QProgressDialog.close()失败,进度条关闭感觉失败了,无法彻底关闭

开发环境&#xff1a;我是在deepin&#xff08;深度&#xff09;系统下开发的&#xff0c;在我本机上&#xff0c;一点问题也没有&#xff0c;但是我移植到了ubantu的机子上&#xff0c;就偶尔出现出个问题&#xff0c;出现了一个模态框&#xff0c;需要重启软件才能关闭。 问题…

Vue的computed和watch的区别是什么?

一、computed介绍 computed 用来监控自己定义的变量&#xff0c;该变量在 data 内没有声明&#xff0c;直接在 computed 里面定义&#xff0c;页面上可直接使用。 //基础使用 {{msg}} <input v-model"name" /> //计算属性 computed:{msg:function(){return …

【MySQL】MySQL日志系统以及InnoDB背后的技术(MySQL专栏启动)

&#x1f4eb;作者简介&#xff1a;小明java问道之路&#xff0c;专注于研究 Java/ Liunx内核/ C及汇编/计算机底层原理/源码&#xff0c;就职于大型金融公司后端高级工程师&#xff0c;擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。 &#x1…

Java基于springboot+vue的个人博客网站 前后端分离

随着现在网络的快速发展&#xff0c;网上管理系统也逐渐快速发展起来&#xff0c;网上管理模式很快融入到了许多网站的之中&#xff0c;随之就产生了“博客网站”&#xff0c;这样就让博客网站更加方便简单。 对于本博客网站的设计来说&#xff0c;系统开发主要是采用java语言技…

2022国产8K摄像机介绍

摄像机是一种把光学图像信号转变为电信号&#xff0c;以便于存储或者传输的设备。当我们拍摄一个物体时&#xff0c;此物体上反射的光被摄像机镜头收集&#xff0c;使其聚焦在摄像器件的受光面&#xff08;例如摄像管的靶面&#xff09;上&#xff0c;再通过摄像器件把光转变为…

N-HiTS: Neural Hierarchical Interpolation for Time Series Forecasting

N-HiTS: Neural Hierarchical Interpolation for Time Series Forecasting 神经预测的最新进展加速了大规模预测系统性能的提高。然而,长期预测仍然是一项非常困难的任务。影响这项任务的两个常见挑战是预测的波动性和它们的计算复杂性。本文提出N-HiTS,一种通过结合新的分层…