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

任务的可中断执行是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()
}

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

const MaxTime = 5 // 5ms

function shouldYield() {
  return performance.now() - lastRecordingTime /*任务开始时间*/ > MaxTime
}

// 通过 shouldYield 函数来判断是否需要中断
while(!shouldYield()) {
  // 每个阶段都是执行一下函数
  doWorkForCurrentLoop()
}

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

任务中断之后继续执行

继续执行是指在之前的状态上继续执行。举个🌰,比如:你需要打印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) {}

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

调度器实现

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

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

MessageChannel实现微任务执行

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

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

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

执行队列里的任务

let lastRecordingTime = -1 // 任务开始时间
function workLoop() {    
    while(taskQueue.length) {
        const top = taskQueue[0]
        // 记录开始时间
        const startTime = lastRecordingTime = performance.now()
        // 执行任务
        top.callback(startTime)

        // 执行完 弹出任务
        top.shift()
    }
}

队列任务执行的可中断

这里跟上述的可中断类似,也可以通过 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()
    }
}

任务可继续执行

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

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
}

微任务实现任务队列执行

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)

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

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

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

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值