react- Scheduler

532 阅读4分钟

这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战

时间切片原理

时间切片的本质是模拟实现requestIdleCallback 除去“浏览器重排/重绘”,下图是浏览器一帧中可以用于执行JS的时机。

一个task(宏任务) -- 队列中全部job(微任务) -- requestAnimationFrame -- 浏览器重排/重绘 -- requestIdleCallback

requestIdleCallback是在“浏览器重排/重绘”后如果当前帧还有空余时间时被调用的。 浏览器并没有提供其他API能够在同样的时机(浏览器重排/重绘后)调用以模拟其实现。

唯一能精准控制调用时机的APIrequestAnimationFrame,他能让我们在“浏览器重排/重绘”之前执行JS 所以 Scheduler时间切片功能是通过task(宏任务)实现的.

Reactrender阶段,开启Concurrent Mode时,每次遍历前,都会通过Scheduler提供的shouldYield方法判断是否需要中断遍历,使浏览器有时间渲染:


function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

Schdeduler中,为任务分配的初始剩余时间为5ms。 随着应用运行,会通过fps动态调整分配给任务的可执行时间。

优先级调度

Scheduler是独立于React的包,所以他的优先级也是独立于React优先级的。

Scheduler对外暴露了一个方法unstable_runWithPriority。这个方法接受一个优先级与一个回调函数,在回调函数内部调用获取优先级的方法都会取得第一个参数对应的优先级

function unstable_runWithPriority(priorityLevel, eventHandler) {
  switch (priorityLevel) {
    case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:
      break;
    default:
      priorityLevel = NormalPriority;
  }

  var previousPriorityLevel = currentPriorityLevel;
  currentPriorityLevel = priorityLevel;

  try {
    return eventHandler();
  } finally {
    currentPriorityLevel = previousPriorityLevel;
  }
}

Scheduler内部存在5种优先级。

React内部凡是涉及到优先级调度的地方,都会使用unstable_runWithPriority

比如,我们知道commit阶段是同步执行的。可以看到,commit阶段的起点commitRoot方法的优先级为ImmediateSchedulerPriority

ImmediateSchedulerPriorityImmediatePriority的别名,为最高优先级,会立即执行。

优先级的意义

Scheduler对外暴露最重要的方法便是unstable_scheduleCallback 该方法用于以某个优先级注册回调函数。不同优先级意味着不同时长的任务过期时间。

scheduler调度算法

首先,要明确几点:

  • scheduler是用来做任务调度的

  • 所有任务在一个调度生命周期内都有一个过期时间与调度优先级,但是调度优先级最终还是会转换为过期时间,只是过期时间长短的问题,过期时间越短代表越饥饿,优先级也就越高,但已经过期了的任务也会被视为饥饿任务

  • requestAnimationFrameWithTimeout,这是React scheduler的一个超强的函数,它是解决网页选项卡如果在未激活状态下requestAnimationFrame不会被触发的问题,这样的话,调度器是可以在后台继续做调度的,一方面也能提升用户体验,同时后台执行的时间间隔是以100ms为步长,这个是一个最佳实践,100ms是不会影响用户体验同时也不影响CPU能耗的一个折中时间间隔

  • 调度优先级分为:

    • 立即执行优先级,立即过期
    • 用户阻塞型优先级,250毫秒后过期
    • 空闲优先级,永不过期,可以在任意空闲时间内执行
    • 普通优先级,5秒后过期
  • 一个调度生命周期分为几个阶段

    • 调度前
      • 注册任务队列(环状链表,头接尾,尾接头),按照过期时间从小到大排列,如果当前任务是最饥饿的任务,则排到最前面,并立即开始调度,如果并不是最饥饿的任务,则放到队列中间或者最后面,不做任何操作,等待被调度
    • 调度准备
      • 通过requestAnimationFrame在下一次屏幕刚开始刷新的帧起点时计算当前帧的截止时间(33毫秒内)
      • 如果不超过当前帧的截止时间且当前任务没有过期,进入任务调度
      • 如果已经超过当前帧的截止时间,但没有过期,进入下一帧,并更新计算帧截止时间,重新判断时间(轮询判断),直到没有任何过期超时或者超时才进入任务调度
      • 如果已经超过当前帧的截止时间,同时已经过期,进入过期调度
    • 正式调度
      • 执行调度
        • 在当前帧的截止时间前批量调用所有任务,不管是否过期
      • 过期调度
        • 批量调用饥饿任务或超时任务的回调,删除任务节点
    • 调度完成
    • 检查任务队列是否还有任务
      • 先执行最饥饿的任务
      • 如果存在任务,则进入下一帧,进入下一个调度生命周期。