likes
comments
collection
share

想知道react怎么优化自身性能的?react任务调度源码超硬核保姆级逐行解析!看完面试官随便问~

作者站长头像
站长
· 阅读数 22

1. 前言

在react的fiber出来之前,react的性能一直是被人诟病的一个点,在高性能的场景下react的表现经常不尽人意

在fiber推出后,react的交互体验大大提升,这背后的原因正是fiber与调度器的功劳

如果我们了解了react的任务调度机制,我们对react的认知将会上一个新的台阶,并且调度器在react源码中作为一个单独文件夹存在,根据源码中的MD文档,说不定什么时候react的调度器就会单独成为一个新项目抽离出来,不如趁现在赶紧学起来~!

2 解析

2.1 react的任务调度总览

react中,任务调度是有Scheduler 调度器负责的,它是一个独立的包,与react类似合作关系,可以简单的理解为,react把一堆任务标注好优先级,交给调度器,由调度器来负责判断是否执行、什么时候执行、怎么执行

多个任务时,会根据任务优先级的从高到低执行;单个任务时也不会一次执行完,如果某个任务执行时间过长,会对任务进行暂停并记录当前状态,以便于下次时间切片中继续执行,调度器内部会规定一个时间切片,例如5ms,在这5ms内能执行多少任务就执行多少,执行不完的就先暂停,下次再执行

调度器的两大核心就是上述的任务优先级和时间切片,接下来将会围绕这两个核心展开讨论

2.2 什么是任务

在scheduler中,可以简单的理解为fiber传递过来的一个回调函数,这个回调函数接收一个参数,该参数为当前任务是否过期的布尔值,该回调函数执行后,会根据传入的时间戳来’适量‘执行任务,在某些情况下这个回调函数会返回一个function,这时意味着当前任务还没执行完,但是时间到了需要暂停了

const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {

而在调度其中,会使用taskQueue和timerQueue 来存储任务,前者存储立即执行的任务,后者存储可延迟执行的任务

// Tasks are stored on a min heap
var taskQueue: Array<Task> = [];
var timerQueue: Array<Task> = [];

在源码中任务的初始化定义是这样的

var newTask: Task = {
    id: taskIdCounter++,//用于过期时间一致时的二次比较,id一定为唯一值
    callback,//真正需要执行的任务函数
    priorityLevel,//任务优先级 参与计算任务过期时间
    startTime,
    expirationTime,
    sortIndex: -1,//先用过期时间作比较,后面会把sortIndex赋值为过期时间
  };

2.3 一切任务的起点:unstable_scheduleCallback

任何任务想添加到调度器中都会使用*unstable_scheduleCallback* 方法处理任务,这个方法的为整个任务调度流程的入口,决定了当前任务是放到timerQueue中还是taskQueue中,同时也控制了任务调度的开始和重启,具体源码如下:

function unstable_scheduleCallback(
  priorityLevel: PriorityLevel,
  callback: Callback,
  options?: {delay: number},
): Task {
  var currentTime = getCurrentTime();

  var startTime;
  //判断是否需要给startTime增加delay时间
  //这里其实react内部还没有启用delay这个参数,以前实验版有用到,所以其实目前不会用到这部分逻辑
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }
  //根据优先级类型增加延迟时间
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
	//立即执行的延迟时间为负数
      timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;// 无限大 永远不执行
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;// 10000
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;// 5000
      break;
  }

  var expirationTime = startTime + timeout;

  var newTask: Task = {
    id: taskIdCounter++,//用于过期时间一致时的二次比较,id一定为唯一值
    callback,//任务函数
    priorityLevel,//任务优先级 参与计算任务过期时间
    startTime,
    expirationTime,
    sortIndex: -1,//先用过期时间作比较,后面会把sortIndex赋值为过期时间
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }


  // 下面的if...else判断各自分支的含义是:

  // 如果任务未过期,则将 新任务 放入timerQueue, 由于目前没有启用delay参数,所以目前所有任务都会放到taskQueue中
  // 放入timerQueue后,调用requestHostTimeout,目的是在timerQueue中排在最前面的任务的开始时间的时间点检查任务是否过期,
  // 过期则立刻将任务加入taskQueue,开始调度

  // esle: 如果任务已过期,则将 新任务 放入taskQueue,调用requestHostCallback,
  // 开始调度执行taskQueue中的任务
  if (startTime > currentTime) {
    // 任务未过期,以开始时间作为timerQueue排序的依据
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // 如果现在taskQueue中没有任务,并且当前的任务是timerQueue中排名最靠前的那一个
      // 这个时候当前任务将会timerQueue中最先转移到taskQueue中的任务,requestHostTimeout来处理整个任务
      if (isHostTimeoutScheduled) {
        // 因为即将调度一个requestHostTimeout,所以如果之前已经有timerQueue中的任务在requestHostTimeout等待执行了,那么取消掉,用requestHostTimeout重新执行当前任务
		//因为requestHostTimeout专门处理timerQueue中过期时间最短的任务,如果代码执行到这里,说明当前任务就是过期时间最短的任务
		//所以就要把之前那个 用于处理之前那个过期时间最短的任务的requestHostTimeout定时器清理掉
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // 调用requestHostTimeout,在过到达过期时间时实现任务的转移,开启调度
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 任务已经过期,以过期时间作为taskQueue排序的依据
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);

    // 开始执行任务,这里可以看出真正的任务执行者是flushWork
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

2.3.1所有任务的暂存仓库:taskQueue和timerqueue

所有进入到调度其中的任务都会暂存在tastQueue中或者timerQueue中,taskQueue存储过期了的任务,timerQueue中存储未过期任务,调度器真正执行的为taskQueue中的任务

并且这两个任务池都会进行优先级排序,过期时间越近的优先级越高,当timerqueue中的一个任务过期时,就会把该任务放到taskqueue中,在调度器底层中,是通过advanceTimers 函数来将timerqueue中的过期任务加入到taskqueue中的

function advanceTimers(currentTime: number) {
  //拿到timerQueue中优先级最高的任务
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      //timer.callback为null,就意味着这个任务是一个无效任务,不需要执行,直接扔了而就可以了
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // 当前任务的开始时间已经小于当前时间了,说明已经过期了,需要立即放到taskQueue中等待依次执行
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // 进到这里了,就说明当前堆顶的任务即是有效任务,而且还没过期,则不用再继续判断剩下的任务了
      return;
    }
    //最后,再次把timerQueue中优先级最高的任务赋值给timer,用于下一次while循环判断
    timer = peek(timerQueue);
  }
}

上述代码中的push方法和pop方法主要用于调整任务池中的任务顺序,但是和js中Array的push、pop不一样,而是采用另一种数据结构来维护这两个任务池中的优先级

2.3.2 任务优先级

taskQueue和timerQueue这两个数据结构在底层是以数组的形式呈现的,我们以taskQueue为例,taskQueue数组中有多个任务,那么如何确定哪个任务先执行哪个任务后执行呢?

我们先来看依照入队顺序来进行任务优先级排序怎么样,这个方案听起来不错,但是假设有这样一个场景:此时先来了任务1渲染整个页面,然后来了任务2渲染骨架屏,显然骨架屏渲染速度更

快优先级更高,但是由于任务1先进入队列,所以需要等待任务1执行完毕后再执行任务2,此时这种顺序就很不合理,这时就需要对新来的任务进行排序,按照一定的顺序来执行

2.3.3scduler中的任务池数据结构:堆( 最小堆)

在react内部是采用的堆结构来维护任务池的优先级,具体来说是最小堆,也叫小顶堆,在react调度器相关逻辑中有一个js文件SchedulerMinHeap.js 专门负责最小堆相关实现逻辑

为什么使用最小堆而不使用最大堆呢?这是因为taskQueue的newTask中的排序用的是sortIndex,而这个值取自过期时间expirationTime,也就是说,优先级越高的任务,在任务队列中过期时间越小,我们这里可以看到react对于最小堆的比较逻辑:

function compare(a: Node, b: Node) {
  // /首先对比时间戳,时间戳越近的优先级就越靠前,如果时间戳一致,则对比任务的唯一值id,id越小任务出现的越早
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}

而小顶堆中,堆顶的元素正是当前数据集合中最小的元素,按照堆结构的特性,取出堆顶的元素的速度是很快的,所以采用小顶堆

2.3.4 为什么使用堆

既然最终的需求是需要把任务池!!#ff0000 (taskQueue数组)!!按照任务优先级排序,为什么采用堆来处理,而不是直接使用Array.sort来处理呢? 我们将这个问题拆解为以下几个步骤: 需求分析→方案对比→确定方案

2.3.4.1需求分析

我们当前的场景是,有大量的任务(可以简单理解为一个有一定性能消耗的函数)需要按照一定的顺序(执行优先级)进入一个集合中(taskqueue),并且随着时间的推移会对这个任务池频繁进行任务顺序交换及删除的操作,换取话说,整个集合是一个动态的任务池

2.3.4.2方案对比

Array.sort 优:使用方便,可维护性高,实现简单 缺:每次有新任务进来都要对整个任务池进行排序,性能差 缺:相较于Array.sort,实现复杂 优:由于堆结构的特性,每次任务来时不需要对整个任务排序,只会将新任务放入堆数组的末尾,然后递归向上对比并交换位置,直到根节点

2.3.4.3确定方案

综上所述,显然实现复杂度的优先级远低于性能要求,所以选择堆来实现

2.3.4.4确定方案

最后我们发现堆结构对于目前需求来说效率最高,所以最够选择堆结构

2.3.5 堆结构科普

这里我们用最小堆为例,虽然在js中,对于堆结构是通过数组实现的,但是我们可以抽象的把堆理解为一个完全二叉树

想知道react怎么优化自身性能的?react任务调度源码超硬核保姆级逐行解析!看完面试官随便问~

每次来新数据了,就会放入当前这个完全二叉树的末尾

想知道react怎么优化自身性能的?react任务调度源码超硬核保姆级逐行解析!看完面试官随便问~ 然后依次和父节点对比,如果比父节点小,小,则与父节点交换位置,交换后如果还是比父节点的父节点小,再次交换,以此类推,如果新插入的元素是当前集合中的最小元素,那么在经过多轮交换后,这个新结点就会出现在根节点

想知道react怎么优化自身性能的?react任务调度源码超硬核保姆级逐行解析!看完面试官随便问~ 移除节点则反之,将最后一个节点与根节点交换位置,然后再将根节点与子节点对比并且一次交换位置

想知道react怎么优化自身性能的?react任务调度源码超硬核保姆级逐行解析!看完面试官随便问~

2.4 任务调度流程

2.4.1非过期任务处理者:requestHostTimeout

requestHostTimeout 本身的动作只是在特定时间后执行一个回调函数

function requestHostTimeout(
  callback: (currentTime: number) => void,
  ms: number,
) {
  // $FlowFixMe[not-a-function] nullable value
  taskTimeoutID = localSetTimeout(() => {
    callback(getCurrentTime());
  }, ms);
}

这个特定时间指的就是timerQueue中优先级最高的(最先过期的)任务的过期时间,而这个回调函数其实就是handleTimeout ,所以针对未过期任务的处理者其实是handleTimeout,其核心任务就是,在timerQueue中最先过期的任务过期时间到了时就移动到taskQueue中并且执行任务调度,否则再次使用requestHostTimeout进行下一轮等待

function handleTimeout(currentTime: number) {
  isHostTimeoutScheduled = false;
  //将timerQueue中的过期任务移动到taskQueue中
  advanceTimers(currentTime);
  //检查是否已经开始调度
  if (!isHostCallbackScheduled) {
    //能进入这里,说明当前还没没有进行任务执行动作,因为在任务调度流程开始时,会进行一次requestHostTimeout清除
    if (peek(taskQueue) !== null) {
      // 检查taskQueue中是否有任务
      //因为taskQueue中装的全是过期任务,那么如果有现在立即requestHostCallback开始调度执行任务
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      //由于在该函数一开始时就用advanceTimers检查过了timerQueue中所有已过期任务,如果进入到这里,就是说明没有任务过期
      //那么再次调用 `requestHostTimeout` 重复这一过程,这里`requestHostTimeout`的执行时间就是timerQueue中最先过期的任务的时间差
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

这里注意 react内部对于时间的要求都是尽可能的精确,所以会优先使用performanceAPI来获取精确的当前时间,如果获取不到才会时候Date来获取当前时间

if (hasPerformanceNow) {
  const localPerformance = performance;
  getCurrentTime = () => localPerformance.now();
} else {
  const localDate = Date;
  const initialTime = localDate.now();
  getCurrentTime = () => localDate.now() - initialTime;
}

2.4.2 任务调度入口:requestHostCallback

调度器通过requestHostCallback 让任务进入调度流程, 从(2.2)最末尾中能看出,当任务为非延迟执行任务时,则会通过requestHostCallback开始调度,从源码中能看出其实该方法内部就是进行了schedulePerformWorkUntilDeadline的执行

function requestHostCallback(
  callback: (hasTimeRemaining: boolean, initialTime: number) => boolean,
) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
  //判断是否正在进行任务调度流程,如果没在进行,才继续流程
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

这里我们就看看schedulePerformWorkUntilDeadline 是怎么执行调度的

let schedulePerformWorkUntilDeadline;

if (typeof localSetImmediate === 'function') {
  //优先使用setImmediate,这个api仅有node环境和老IE环境支持
  // 在node环境中,MessageChannel强行退出进程可能会丢失部分任务
  // 虽然这是一个非标准的API,但是在语义上该所表达执行时机比settimeout好.
  //而且node环境不像浏览器中有dom交互,也没有屏幕刷新率,所以也就没必要时间切片,不用判断是否超出了时间切片的限制
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  //MessageChannel更适合浏览器环境
  //而且相较于settimeot,settimeout会有多层嵌套场景下最小4ms延迟的特性,而react时间切片的粒度才只有5ms,settimeout所浪费的时间是不能接受的
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  // 实在不行了 才用settimeout
  schedulePerformWorkUntilDeadline = () => {
    localSetTimeout(performWorkUntilDeadline, 0);
  };

从上面源码能看出在当前流程执行中,下一步就是交给performWorkUntilDeadline来执行

2.4.3任务调度流程开始执行的入口:performWorkUntilDeadline

performWorkUntilDeadline涉及到调用任务执行函数去执行任务,这个过程中会涉及任务的中断和恢复任务完成状态的判断,这是scheduler对于任务进度把控的核心逻辑

performWorkUntilDeadline作为一个执行者,他的行为可以理解为以下几个动作:

  • 根据时间切片时长执行任务
  • 单个切片时间截至时暂停任务并通知scheduler再次创建一个执行者来执行下次任务

首先 我们来观察performWorkUntilDeadline 里在干什么

const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // 记录当前时间
    startTime = currentTime;
    const hasTimeRemaining = true;
    let hasMoreWork = true;
    try {
      //关键!整个任务调度执行流程执行到这里,下一步是通过scheduledHostCallback来执行任务
      //scheduledHostCallback其实就是workLoop,该方法返回一个布尔值,表示taskQueue中的任务是否全都执行完了
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      if (hasMoreWork) {
        //进入到这里,说明taskQueue中还有任务没执行完,则通过schedulePerformWorkUntilDeadline通知下一次宏任务继续执行
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }
  needsPaint = false;
};

2.4.4 任务调度中的任务执行入口:scheduledHostCallbackflushWork

我们在2.3.2中的requestHostCallback中能看到,其实scheduledHostCallback就是requestHostCallback接受的回调函数,也就是flushWork

2.4.5 真正干活的:workLoop

其实workloop是由flushWork来调用的,真正在一遍遍执行任务的就是这个函数,我们这里先来分析这个干活的函数,然后再分析flushWork,这样后面分析flushWork会你比较清晰

/**
 * 
 * @param {boolean} hasTimeRemaining 是否还有剩余时间,在当前版本中为常量true
 * @param {*} initialTime 当前方法被调用时的时间,在当前版本中为currentTime函数的返回值
 * @returns 
 */
function workLoop(hasTimeRemaining: boolean, initialTime: number) {
  let currentTime = initialTime;
  //将timerQueue中的过期任务移动到taskQueue中
  advanceTimers(currentTime);
  //拿到taskQueue中优先级最高的任务
  currentTask = peek(taskQueue);
  //当taskQueue堆的堆顶有任务,且调度器不是debugger模式且没有暂停调度器
  //注意,可以通过unstable_pauseExecution和unstable_continueExecution暂停和开启调度器,这个由fiber决定
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      //currentTask.expirationTime > currentTime: 当当前任务的过期时间大于当前时间,说明这个任务不着急执行
      //(!hasTimeRemaining || shouldYieldToHost()): 没有剩余时间了或者经过shouldYieldToHost判断本次任务执行已经超出了时间切片的时间了
      break;
    }
    // 拿到当前任务
    const callback = currentTask.callback;
    //当前任务为一个function才执行 否则为无效任务
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      if (enableProfiling) {
        markTaskRun(currentTask, currentTime);
      }
      //开始执行任务
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
	  //返回一个函数 说明当前任务没执行完,但是函数内部判定没时间了,就先返回一个函数,下次再从这个函数开始继续执行,也就是常说的任务调度中的任务中断
      if (typeof continuationCallback === 'function') {
        //将返回回来的函数作为新的任务函数重新赋值
        currentTask.callback = continuationCallback;
        if (enableProfiling) {
          markTaskYield(currentTask, currentTime);
        }
        //再次判断一次timerQueue中的任务是否有过期的
        advanceTimers(currentTime);
        //这里直接返回true,停止workLoop的执行,意味着本次时间切片内的任务调度执行需要结束了
        return true;
      } else {
        //进入到这里,表示taskQueue中当前优先级最高的任务已经执行完毕,此时对taskQueue小顶堆进行顺序调整,以便于当前while循环的下次执行
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
        //再次进行顺序调整
        advanceTimers(currentTime);
      }
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  // currentTask为true,证明taskQueue中还有任务没执行完毕,但是本次任务的执行时间已经超过调度器的时间切片了
  //所以告知performWorkUntilDeadline,hasMoreWork
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

这里注意,在workloop的while循环中有一个地方可以判定是否需要结束这个while循环,这里有一个条件currentTask.expirationTime > currentTime ,这里这次判断看起来多此一举,因为在unstable_scheduleCallback这个每个任务进入到调度器的必经之路中,如果任务可以被放到taskQueue中,那么这个任务的排序依据就是他的过期时间 ,由于过期时间一定大于任务开始时间,所以在workLoop中的判断条件currentTask.expirationTime > currentTime 就会显得多此一举 !!#ff0000 其实不然!!!在schduler中,还有一个地方会对任务的option进行操作,那就是advanceTimers ,在该函数中,如果一个timerQueue中的任务满足开始时间小于等于当前时间(timer.startTime <= currentTime) ,则会把该任务放到taskQueue中,而在进行这个操作时,将任务的sortIndex复制为了任务的过期时间 ,这就导致了刚才我们说那个while循环里的break条件判断的地方需要额外判断currentTask.expirationTime > currentTime,由于advanceTimers中的操作,导致一个taskQueue中的任务虽然开始时间小于了当前时间,但是过期时间不一定小于当前时间

2.4.6 任务调度中的执行者:flushWork

flushWork的任务就是调用真正干活的方法workLoop,并且判定如果干活的函数说还有活儿没干完,但是没时间了,那么就在下一次宏任务中再次执行任务调度,接下来看源码分析:

/**
 * 
 * @param {boolean} hasTimeRemaining 是否还有剩余时间,在当前版本中为常量true
 * @param {*} initialTime 当前方法被调用时的时间,在当前版本中为currentTime函数的返回值
 * @returns 
 */
function flushWork(hasTimeRemaining: boolean, initialTime: number) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  // We'll need a host callback the next time work is scheduled.
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    // 如果之前已经有timerQueue中的任务在requestHostTimeout等待执行了,那么取消掉
    //requestHostTimeout真正执行的是handleTimeout,而该函数的任务就是将timerQueue中优先级最高的任务在抵达过期时间时,将其放入taskQueue中
    //而在后续的任务执行时,会有很多个阶段进行timerQueue中过期任务移动到taskQueue的动作,所以这里requestHostTimeout的任务就没必要继续了
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
  //当前版本的enableProfiling为常量false,所以下面这个if目前肯定不会进去
    if (enableProfiling) {
      try {
        return workLoop(hasTimeRemaining, initialTime);
      } catch (error) {
        if (currentTask !== null) {
          const currentTime = getCurrentTime();
          // $FlowFixMe[incompatible-call] found when upgrading Flow
          markTaskErrored(currentTask, currentTime);
          // $FlowFixMe[incompatible-use] found when upgrading Flow
          currentTask.isQueued = false;
        }
        throw error;
      }
    } else {
	        //从这里开始,就是真正开始执行任务了
        //从这里可以看出,flushWork的返回值其实就是workLoop的返回值
        //workLoop的返回值在上一节讲到,其实就是当前taskQueue中的任务是否执行完了
      return workLoop(hasTimeRemaining, initialTime);
    }
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
    if (enableProfiling) {
      const currentTime = getCurrentTime();
      markSchedulerSuspended(currentTime);
    }
  }
}

2.5总结整体流程

2.5.1 react与调度器的关系

我们可以将整个模块分为三个部分:任务产生层→任务翻译层→任务执行层

2.5.2 任务产生层

react再一次更新中,通过新老虚拟dom的对比等一系列操作,计算出最小所需更新任务,并且通过lane模型计算出当前任务的优先级

const workInProgressRoot = getWorkInProgressRoot();
  const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

2.5.3 任务翻译层

任务翻译层的主要任务就是把fiber给的任务优先级翻译为schduler认识的任务优先级

let schedulerPriorityLevel;
    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediateSchedulerPriority;
        break;
      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingSchedulerPriority;
        break;
      case DefaultEventPriority:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
      case IdleEventPriority:
        schedulerPriorityLevel = IdleSchedulerPriority;
        break;
      default:
        schedulerPriorityLevel = NormalSchedulerPriority;
        brea

2.5.4 任务执行层

最后再将任务交给scheduler进行调度执行

const newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
	........

2.6总结

我本次分享的主要内容就是scheduler内部的一些动作 经过了上面这些分析,我们可以发现在react中,为了充分压榨cpu性能且不影响用户体验,react想了很多办法,scheduler就是其中很重要的一环 我们可以把scheduler的主要任务总结为以下两点:

  • 它将fiber给他的一个个的小任务排序存储,并且在浏览器每次空闲时成批量的检查任务池并执行
  • 执行时也时时刻刻检查单次任务流程是否超过了时间切片的时间限制,以防出现页面卡顿影响用户体验

最后,我们在scheduler的README.md中,能看到react对scheduler未来的展望,react希望scheduler在未来可以作为一个单独的库抽离出来,这种“任务切片后按需处理”的方案,不光适用于react目前的需求,对于一些性能消耗较大导致页面卡顿的场景,相信在scheduler的加持下会有更好的表现

最后最后,作为一篇学习分享文章,相信肯定有没有说到的或者我本人理解有误的地方,欢迎大家一起讨论哦!~~

转载自:https://juejin.cn/post/7238172236185059385
评论
请登录