scheduler模块

作者: jluemmmm | 来源:发表于2021-12-12 20:04 被阅读0次

    scheduler模块用于管理重绘完成后回调的执行逻辑。从输出分析,对整个调度过程进行梳理。

    基础前提

    浏览器渲染与事件循环

    浏览器采用多进程架构,包含浏览器主进程、渲染进程、插件进程、GPU进程。每开启一个tab时浏览器就会开启一个渲染进程,该进程里包含多个线程:负责运行jsdomcss计算和页面渲染的主线程,运行worker的工作线程等。主线程解析html时,遇到script标签,会暂停html的解析,并开始加载、解析并执行js代码、为了调度事件、用户交互、渲染、网络请求这些操作,主线程会通过事件循环来处理。事件循环的过程为:

    • 同步任务

    • 一个宏任务

    • 清空微任务队列

    • 判断是否渲染视图(是否有重排、重绘、渲染间隔是否达到16.7ms等),为真则渲染视图,否则跳至步骤1,页面渲染前调用requestAnimationFrame回调函数,最后判断是否启动空闲时间算法,如果启动就调用requestIdleCallback

    常见的宏任务:事件回调、xhr回调、定时器、I/OMessageChannel

    常见的微任务:PromiseGeneratorAsync/AwaitMutationObserver

    时间片

    js在浏览器中的执行是单线程的,长时间的js任务执行可能会阻塞其他浏览器任务,如页面渲染、用户交互等,有可能会造成用户的卡顿感。schedule中采用时间分片的策略,将任务细化为不同的优先级,利用浏览器的空闲时间进行任务的执行保证UI操作的流畅。浏览器的调度API主要分为两种,高优先级的requestAnimationFrame与低优先级的requestIdleCallback

    js任务分解到时间片中执行后,一次时间循环最多只执行一个时间片,若还有未完成的任务,将这些任务放到后面的事件循环的时间片中执行,保证不会阻塞其他的浏览器任务。

    requestAnimationFrame

    requestAnimationFrame传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。回调函数执行次数通常是每秒60次,在大多数遵循w3c建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。大多数浏览器中,当 requestAnimationFrame运行在后台标签页或隐藏的iframe中时,requestAnimationFrame会被暂停调用。

    requestAnimationFrame函数接收一个接收DOMHighResTimeStamp参数的callback函数作为参数,返回一个requestIdcancelAnimationFrame以取消。

    requestIdleCallback

    浏览器每秒一般60帧,帧与帧的间隔成为时间片,长度为 1000 / 6016ms,如果一帧渲染完成的时间小于16ms,这个时间片就有空闲时间。空闲时间会被执行requestIdleCallback的回调函数。当超过timeout时间还不执行callback时,callback将会被强制执行,造成的后果是,阻塞本地渲染,延长渲染时间,造成卡顿、延迟等。

    callback函数接收IdleDeadline接口类型的参数,是一个对象,包含两个属性

    • didTimeout,布尔值,表示任务是否超时

    • timeRemaining,表示当前时间片剩余的时间。

    requestIndleCallback会返回一个id,传入cancelIdleCallback可结束对应的回调。

    使用时间片的实现:

    scheduler每执行一次performWorkUntilDeadline函数表示执行了一个时间片,执行该函数前会通过MessageChannelsetTimeout将该函数放入宏任务队列,优先使用MessageChannel

    每执行一个时间片时,将时间片长度 yieldInterval和过期时间deadline放入执行的任务中,并通返回值判断是否存在未执行完的任务(表示时间片已用完)。若存在为执行完的任务,则让任务在下次事件循环继续执行。

    export {
     ImmediatePriority as unstable_ImmediatePriority, // 1
     UserBlockingPriority as unstable_UserBlockingPriority, // 2
     NormalPriority as unstable_NormalPriority, // 3
     IdlePriority as unstable_IdlePriority, // 5
     LowPriority as unstable_LowPriority, // 4
     unstable_runWithPriority,
     unstable_next,
     unstable_scheduleCallback,
     unstable_cancelCallback,
     unstable_wrapCallback,
     unstable_getCurrentPriorityLevel,
     shouldYieldToHost as unstable_shouldYield,
     unstable_requestPaint,
     unstable_continueExecution,
     unstable_pauseExecution,
     unstable_getFirstCallbackNode,
     getCurrentTime as unstable_now,
     forceFrameRate as unstable_forceFrameRate,
    };
    export const unstable_Profiling = enableProfiling
     ? {
         startLoggingProfilingEvents,
         stopLoggingProfilingEvents,
       }
     : null;
    

    输出函数分析

    任务优先级

    react内对任务优先级的定义。Scheduler中任务有不同的优先级,每个优先级有对应的过期时间,在生成任务时根据优先级和创建时间生成任务的过期时间,任务过期后才会放入taskQueue执行,否则放入timerQueue等待执行。

    优先级 含义 过期时间 过期时间的值
    NoPriority 0 无优先级
    ImmediatePriority 1 最高优先级 IMMEDIATE_PRIORITY_TIMEOUT -1
    UserBlockingPriority 2 用户阻塞型优先级 USER_BLOCKING_PRIORITY_TIMEOUT 250
    NormalPriority 3 普通优先级 NORMAL_PRIORITY_TIMEOUT 5000
    LowPriority 4 低优先级 LOW_PRIORITY_TIMEOUT 10000
    IdlePriority 5 空闲优先级 IDLE_PRIORITY_TIMEOUT maxSigned31BitInt = Math.pow(2, 30) - 1

    环境中设置变量分析

    
    //任务存储在小顶堆上
    var taskQueue = [] // 任务队列
    var timerQueue = []; // 延时任务队列
    var taskIdCounter = 1; // 递增id计数器, 用于维护插入顺序
    var isSchedulerPaused = false; // 暂停调度程序,用于调试
    var currentTask = null; // 当前任务
    var currentPriorityLevel = NormalPriority; //3 当前执行任务的优先级
    var isPerformingWork = false; // 是否正在执行任务,在执行工作时设置的, 以防止重新进入
    var isHostCallbackScheduled = false; // 是否有主任务正在执行,是否调度了 taskQueue, isHostCallbackScheduled为true后才把时间片放到宏任务队列,之后开始执行任务
    var isHostTimeoutScheduled = false; // 是否有延时任务正在执行,是否调度了 timerQueue, 设置了 timeout回调
    

    SchedulerHostConfig

    let requestHostCallback; // 请求回调
    let cancelHostCallback; // 取消回调
    let requestHostTimeout; // 请求超时
    let cancelHostTimeout; // 取消超时
    let shouldYieldToHost;
    let requestPaint; // 请求绘制
    let getCurrentTime; // 获取当前时间, 优先用 performance.now(), 或者用 Date.now() - 初始时间
    let forceFrameRate; // 强制帧率
    
    • 如果Scheduler运行在非DOM环境中,使用setTimeout回退到一个简单的实现。环境中检测不到window或者不支持MessageChannel时:

      • requestHostCallback

      • cancelHostCallback

      • requestHostTimeout

      • cancelHostTimeout

      • shouldYieldToHost

      • requestPaint

      • forceFrameRate

    unstable_runWithPriority

    unstable_runWithPriority(*priorityLevel*, *eventHandler*)主要逻辑:

    • currentPriorityLevel设置为priorityLevel,然后执行eventHandler

    • 最后将currentPriorityLevel改回之前的值

    unstable_next

    unstable_next(*eventHandler*)主要逻辑:

    • currentPriorityLevel高于NormalPriority情况下设置为NormalPriority,否则保持当前优先级

    • 执行eventHandler

    • 最后将currentPriorityLevel改回之前的值

    unstable_scheduleCallback

    整体流程:

    • scheduler中任务有不同优先级,每个优先级有对应的过期时间,在生成任务根据优先级和创建事件生成任务的过期时间,任务过期后才会放入taskQueue执行,否则放入timerQueue等待执行

    • TaskQueueTimerQueue是两个用小顶堆实现的具有优先级的任务队列。TaskQueue中优先级的索引是expirationTimeTimerQueue中优先级的索引使用的是startTimeSchedulerMinHeap.js中实现了对小顶堆的peekpoppush方法。使用advanceTimers方法可以依据指定的时间和任务的开始时间将TimerQueue中的任务更新到TaskQueue中,更新时会同时更新索引使用的值为expirationTime

    • 通过unstable_scheduleCallback添加任务,生成一个任务对象。任务对象包含任务id、任务执行函数、优先级、开始时间、过期时间和在队列中的顺序。任务通过传入参数中的delay属性值来判断该任务是同步任务还是异步任务。

    • 若生成的任务是同步任务,则将该任务推入taskQueue

      • 如果当前taskQueue是未被调度且任务未被执行,则使用requestHostCallback调用flushWork方法

      • requestHostCallback方法内会触发message事件,performWorkUntilDeadline函数作为message事件的回调将推入事件循环的宏任务队列

      • performWorkUntilDeadline方法会执行一个时间片的任务,时间片用完后会判断是否还有未执行的任务,如果有则再次触发message事件

      • flushWork先取消timerQueue的回调,之后设置isPerformingWorkfalse,并调用workLoop方法执行taskQueue中的任务

      • workLoop会不断取出taskQueue中的任务,直到执行完所有的任务或者执行完所有超时的任务且时间片已用完。最后若存在未执行完的任务,则返回true,否则重新设置timerQueue中的回调,并返回false

    • 若生成的任务是异步任务,则将任务推入timerQueue。如果当前taskQueue为空且新任务在timerQueue中优先级最高,使用requestHostTimeout调度handleTimeout方法

    • handleTimeout首先判断当前是否在调度taskQueue,若没有在调度,则判断taskQueue是否为空,如果不为空,则调度taskQueue,否则调度timerQueue

    Untitled Diagram.drawio.png

    unstable_scheduleCallback(*priorityLevel*, *callback*, *options*)主要逻辑为,根据输入返回newTask

    • 根据传入的 options更新startTime,根据传入的priorityLevel更新 timeout,然后计算expireTime,定义newTask
    var newTask = {
      id: taskIdCounter++,
      callback,
      priorityLevel,
      startTime,
      expirationTime,
      sortIndex: -1,
    };
    
    • 对于延时任务,startTime > currentTimestartTime设置为sortIndex,将任务添加到timerQueue队列,如果taskQueue为空且timerQueue只有newTask一个延时任务。是否有延时任务正在执行,如果有,清除定时器,否则将isHostTimeoutScheduled设置为true。执行requestHostTimeout,延时处理handleTimeout

      • requestHostTimeout逻辑:接收callbackms,经过ms后执行callback,将getCurrentTime()的值传入.

      • handleTimeout逻辑:isHostTimeoutScheduled设置为false,执行advanceTimers。如果 isHostCallbackScheduledfalse,即没有主任务正在执行,设置isHostCallbackScheduledtrue,将flushWork传递给requestHostCallback

      • advanceTimers逻辑:接收一个参数currentTime,检查timerQueue中的任务,将不再延时的任务添加到taskQueue中。将timerQueue中的堆顶任务弹出,如果不存在timer.callback,任务取消,并且弹出timerQueue,如果timer.startTime <= currentTime,任务弹出timerQueue,并且添加到taskQueue中.

      • flushWork逻辑:接收(hasTimeRemaining, initialTime)两个参数,isHostCallbackScheduled设置为false,如果isHostTimeoutScheduled,设置为false,取消定时器,然后执行workLoop(hasTimeRemaining, initialTime)

      • workLoop逻辑:接收(hasTimeRemaining, initialTime)两个参数,当前任务不为空或任务不停止的情况下,执行循环。当当前任务还没有过期,但是到了deadline,则跳出循环;currentTask有回调的情况下,执行回调,currentTask等于栈顶元素的情况下,将任务从taskQueue中弹出,执行advanceTimers将不再延时的任务添加到任务队列;currentTask没有回调的情况下,将任务从taskQueue中弹出。currentTask为空的情况下,获取timerQueue的栈顶任务,放入requestHostTimeout中。

    https://someu.github.io/2020-11-10/react-scheduler%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/

    https://juejin.cn/post/6889314677528985614

    https://juejin.cn/post/6914089940649246734

    md格式的文件直接粘过来有点丑,待补充

    相关文章

      网友评论

        本文标题:scheduler模块

        本文链接:https://www.haomeiwen.com/subject/lefdfrtx.html