• 人人可懂的任务调度和可中断执行


    任务的可中断执行是reactconcurrence模式下重要的特性。

    what ? 什么是任务的可中断执行

    在执行一个长时间任务的时候,将任务分成多个阶段,每个阶段结束之后去检测当前是否需要停止当前任务,如果需要就停止,不需要就继续执行。

    react在每次任务执行结束,以及fiber构建(fiber构建本身就是一个任务)的每个fiber构建之后(workInProgress每次更新)的时候都会检测是否需要停止当前fiber构建去执行更高优先级的任务

    why ? 为什么需要可中断执行的任务

    主要是javascript的单线程执行机制。如果一个任务执行太长时间,线程一直被使用,如果这时候用户有交互操作,就不能及时得到响应,就有了“页面很卡”的感觉。

    react中的更新过程分为两个阶段:render阶段commit阶段render阶段是来构建fiber树的。如果你的html结构很复杂,层级很深,那么自顶而下的fiber树构建需要花费很长时间。这时候用户的操作,比如Input的输入,按钮的点击等等就被阻塞,而导致用户产生卡顿的感觉。而react源码中就使用了任务的可中断执行,让优先级高的任务先执行,尽快让用户感知变化。

    How ? 怎么让任务可中断执行?

    任务可中断执行的表明意思可以提取出两个重要信息:

    1. 任务执行过程中可以中断
    2. 任务中断之后可以继续执行。

    任务可中断

    如果一个长时间任务的执行抽象成多个阶段,且每个阶段的执行都是类似的工作,那么就可以用while抽象如下:

    while(true) {
      // 每个阶段都是执行以下函数
      doWorkForCurrentLoop()
    }
    
    • 1
    • 2
    • 3
    • 4

    此时,如果在每个while循环之前去检测一下,是否能够继续执行下一个阶段的任务,如果不满足条件就退出while循环,这样就模拟了任务的可中断执行。至于这个检查标准,考虑可中断是为了解决长时间执行导致的阻塞问题,可以设置一个任务可执行的最大时间(react源码中使用的是5ms),超过这个时间就退出。此时代码如下:

    const MaxTime = 5 // 5ms
    
    function shouldYield() {
      return performance.now() - lastRecordingTime /*任务开始时间*/ > MaxTime
    }
    
    // 通过 shouldYield 函数来判断是否需要中断
    while(!shouldYield()) {
      // 每个阶段都是执行一下函数
      doWorkForCurrentLoop()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    此时就实现了任务的可中断执行

    任务中断之后继续执行

    继续执行是指在之前的状态上继续执行。举个🌰,比如:你需要打印1~100,第一次任务执行打印了1 ~ 40的之后中断了执行。 继续执行是指 从打印41开始你的任务。显然这里是需要维护一个状态的。在上面的代码中加入一个状态量wip

    const MaxTime = 5 // 5ms
    let wip = null // 全局的状态量
    
    function shouldYield(startTime) {
        return (performance.now() - startTime) > MaxTime
    }
    
    // start为 mayTaskNeedLongTime 开始执行的时间
    function mayTaskNeedLongTime(startTime) {
        // 通过 shouldYield 函数来判断是否需要中断
        while(wip && !shouldYield(startTime)) {
          // 每个阶段都是执行一下函数
          doWorkForCurrentLoop(wip)
        }
    }
    
    function doWorkForCurrentLoop(wip) {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    while循环封装成一个函数,如上,此时他就是一个任务。接下里需要一个调度器来调度任务队列,也让中断的任务继续执行。

    调度器实现

    首先确认调度器该有的能力:

    1. 任务调度:从任务队列中获取任务并执行,这里有一定标准,react中以过期时间或者说优先级为标准去做调度
    2. 执行时机:如果用同步的方式去执行,那么可中断就没有任务意义。这里用异步任务的方式去执行,这样浏览器的渲染就不会被阻塞。

    MessageChannel实现微任务执行

    const messageChannel = new MessageChannel()
    messageChannel.port1.onmessage = function () {
        // 加入微任务队列
    }
    // 触发 onmessage 执行
    messageChannel.port2.postMessage(null)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    维护任务队列,来管理任务

    const taskQueue = []
    // 一个任务是带有callback的对象
    // 将上述的 mayTaskNeedLongTime 加入task队列
    taskQueue.push({
        callback: mayTaskNeedLongTime
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    执行队列里的任务

    let lastRecordingTime = -1 // 任务开始时间
    function workLoop() {    
        while(taskQueue.length) {
            const top = taskQueue[0]
            // 记录开始时间
            const startTime = lastRecordingTime = performance.now()
            // 执行任务
            top.callback(startTime)
    
            // 执行完 弹出任务
            top.shift()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    队列任务执行的可中断

    这里跟上述的可中断类似,也可以通过 break while循环退出任务执行。

    let lastStartTimeExecutingTask = -1 // 任务开始时间
    function workLoop() {    
        while(taskQueue.length) {
            if (lastStartTimeExecutingTask !== -1 && performance.now() > (lastStartTimeExecutingTask + MaxTime)) {
                break
            }
            const top = taskQueue[0]
            // 记录开始时间
            const startTime = lastStartTimeExecutingTask = performance.now()
            // 执行任务
            top.callback(startTime)
            // 执行完 弹出任务
            top.shift()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    任务可继续执行

    在执行任务的时候,如果当前回调函数的返回值是另一个函数,那么这个返回的函数就是保存之前状态的接下来要继续执行的函数。继续改造代码

    function mayTaskNeedLongTime(startTime) {
        // 通过 shouldYield 函数来判断是否需要中断
        while(wip && !shouldYield(startTime)) {
          // 每个阶段都是执行一下函数
          doWorkForCurrentLoop(wip)
        }
        // 返回一个函数,中断之后继续执行的函数
        return mayTaskNeedLongTime
    }
    
    function workLoop() {    
        while(taskQueue.length) {
            if (lastStartTimeExecutingTask !== -1 && performance.now() > (lastStartTimeExecutingTask + MaxTime)) {
                break
            }
            const top = taskQueue[0]
            // 记录开始时间
            const startTime = lastStartTimeExecutingTask = performance.now()
            
            // 执行任务,同时接受返回值
            /******************继续执行的关键*********************/
            const continuous = top.callback(startTime)
            // 之前的callback已经消费掉,替换为新的callback,可以继续执行
            // 这样下一次的while循环就可以 调用 continuous 函数了,实现可继续执行
            // 同时wip还保存着之前的状态        
            /***************************************/       
            
            if (typeof continuous === 'function') {            
                top.callback = continuous
            } else {
                // 执行完 弹出任务
                top.shift()
            }        
        }
        
        // 表明任务执行完成 队列已清空
        return taskQueue.length === 0
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    微任务实现任务队列执行

    messageChannel.port1.onmessage = function () {
      if (scheduledCallback !== null) {
        const isFinish = scheduledCallback()
        if (isFinish) {
          console.log('任务执行结束')
        } else {
          // 重新让任务进入  微任务队列 执行
          lastStartTimeExecutingTask = -1
          messageChannel.port2.postMessage(null)
        }
      }
    }
    
    function requestCalllback(callback) {
      scheduledCallback = callback
      // 在下一个微任务队列里 执行
      messageChannel.port2.postMessage(null)
    }
    
    requestCalllback(workLoop)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    到此,任务调度,可中断,中断之后的继续执行已完成。

    任务队列里可以添加其他任务,同时可以在任务对象(现在只有callback)中添加其他属性,比如优先级等等,来扩展能力。

    该链接里有上述完整的代码,同时也给任务添加了优先级,实现任务队列执行过程中中断并去执行更高优先级的任务。

  • 相关阅读:
    设计模式:模板方法模式
    【实用技巧】更改ArduinoIDE默认库文件位置,解放系统盘,将Arduino15中的库文件移动到其他磁盘
    TPA4045-ASEMI光伏防回流二极管TPA4045
    Redis的BitMap使用
    力扣hot100题解(python版7-9题)
    中文符号雨python
    Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks - 翻译学习
    Redis单线程
    架构权衡评估方法(ATAM):一种用于软件架构评估的方法,全称为Architecture Tradeoff Analysis Method
    机器学习 | MATLAB实现ELM极限学习机elmtrain参数设定
  • 原文地址:https://blog.csdn.net/qq_41494959/article/details/126206629