requestIdleCallback辅助JS用例

requestIdleCallback 基本概念这里不讲,讲的文章很多。 本章主要参考走进React Fiber世界一文,里面举了一些例子,为了更好理解才有了本篇

一帧做什么

Inpute Events -> JS Timer -> Begin Frame(window resize/scroll/media query change) -> requestAnimationFrame -> Layout -> Paint -> requestIdleCallback

可以看到,requestIdleCallback是在一帧的最后的。它的意思是,在这一帧里(现代浏览器大概16.6ms刷新一帧),如果还有剩余的时间,那么就执行requestIdleCallback里的任务。如果没有时间了,那么就先不执行。

但requestIdleCallback也可以接受一个入参timeout,这就是告诉浏览器,如果到了这个时间,即使这一帧没有剩余时间了,你也要执行我这个任务。 MDN中的解释是:

如果指定了timeout,并且有一个正值,而回调在timeout毫秒过后还没有被调用,那么回调任务将放入事件循环中排队,即使这样做有可能对性能产生负面影响。

也就是说,从执行了 requestIdleCallback 函数起,超过 timeout 时间后,requestIdleCallback 传递的callback,还没有被调用,则强制将 callback 放入到任务队列,而不是放入调用栈调用。

window.requestIdleCallback(callback, { timeout, 1000})

一帧执行

javascript 复制代码
let taskQueue = [
  () => {
    console.log('task1 start')
    console.log('task1 end')
  },
  () => {
    console.log('task2 start')
    console.log('task2 end')
  },
  () => {
    console.log('task3 start')
    console.log('task3 end')
  }
]

const performUnitWork = () => {
  // 取出第一个队列中的第一个任务并执行
  taskQueue.shift()()
}

// deadline是一个对象,有两个属性
// timeRemaining函数可返回此帧还有多少时间供用户使用
// didTimeout 此callback任务是否超时
const workloop = (deadline) => {
  console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`)
  // 如果此帧剩余时间大于0或者已经到了定义的超时时间,且当时存在任务,则直接执行这个任务
  // 如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskQueue.length > 0) {
    performUnitWork()
  }

  // 如果还有未完成的任务,继续调用requestIdleCallback申请下一个时间片
  if (taskQueue.length > 0) {
    window.requestIdleCallback(workloop)
  }
}

requestIdleCallback(workloop)
  • 输出
    几乎同时,输出了下列内容
markdown 复制代码
> 此帧的剩余时间为: 11.7
> task1 start
> task1 end
> task2 start
> task2 end
> task3 start
> task3 end
  • 分析
    原因在于,console.log 执行非常快,deadline.timeRemaining()一直大于0,也就是一直有空余时间,所以在一帧渲染空余时间里,任务都执行完成了

多帧执行

在task1、task2、task3中加入睡眠时间,各自执行时间超过16ms:

javascript 复制代码
const sleep = delay => {
  for (let start = Date.now(); Date.now() - start <= delay;) {}
}

let taskQueue = [
  () => {
    console.log('task1 start')
    sleep(20) // 已经超过一帧的时间(16.6ms),需要把控制权交给浏览器
    console.log('task1 end')
  },
  () => {
    console.log('task2 start')
    sleep(20)
    console.log('task2 end')
  },
  () => {
    console.log('task3 start')
    sleep(20)
    console.log('task3 end')
  }
]

const performUnitWork = () => {
  taskQueue.shift()()
}

const workloop = (deadline) => {
  console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`)
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskQueue.length > 0) {
    performUnitWork()
  }

  if (taskQueue.length > 0) {
    window.requestIdleCallback(workloop)
  }
}

requestIdleCallback(workloop)
  • 输出
    此时从视觉上看,感觉稍微慢了一点点,有种连贯输出内容的感觉,但不是一次性输出的。而实际上,这里也是3帧输出以下内容
markdown 复制代码
> 此帧的剩余时间为: 14.2
> task1 start
> task1 end
> 此帧的剩余时间为: 1.1
> task2 start
> task2 end
> 此帧的剩余时间为: 9
> task3 start
> task3 end
  • 分析
  1. 首次进来,该帧还有"14.2ms",开始执行第一个任务。但该任务执行完后,发现该帧已经不剩时间了,把下个任务推到 idleCallback 里,然后把控制权还给浏览器
  2. 第二帧开始,执行后还剩下"1.1ms",开始执行 idleCallback,也就是第二个任务。同上,归还控制权
  3. 第三帧开始,执行后还剩下"9ms",同上
  • 疑惑
    我在这里就有两个疑惑,一是为什么每帧剩余的时间不一样,我执行的代码都差不多呀?二是这个 console 究竟是什么时候绘制的

重复上述代码

在多执行几次后,竟然概率性输出下面内容:

markdown 复制代码
> 此帧的剩余时间为: 9.7
> task1 start
> task1 end
> 此帧的剩余时间为: 15.4
> task2 start
> task2 end
> 此帧的剩余时间为: 49.1
> task3 start
> task3 end

剩余时间竟然"49.1ms",说好的16ms一帧呢?后来发现,浏览器一帧的时间并不严格是16ms,是可以动态控制的,但为什么会偶现这么大的数值,我至今不太清楚

增大单个任务时长

javascript 复制代码
const sleep = delay => {
    for (let start = Date.now(); Date.now() - start <= delay;) { }
}

let taskQueue = [
    () => {
        console.log('task1 start')
        sleep(2000)
        console.log('task1 end')
    },
    () => {
        console.log('task2 start')
        sleep(2000)
        console.log('task2 end')
    },
    () => {
        console.log('task3 start')
        sleep(2000)
        console.log('task3 end')
    }
]


const performUnitWork = () => {
    taskQueue.shift()()
}

const workloop = (deadline) => {
    console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`)
    while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskQueue.length > 0) {
        performUnitWork()
    }

    if (taskQueue.length > 0) {
        window.requestIdleCallback(workloop);
    }
}

requestIdleCallback(workloop)
  • 输出
markdown 复制代码
// 停顿2s(已经在执行第一个idle任务中)
> 此帧的剩余时间为: 14.6(这时候才打印第一个idle任务执行前的从搜了)
> task1 start
> task1 end
// 停顿2s
> 此帧的剩余时间为: 6.6
> task2 start
> task2 end
//停顿2s
> 此帧的剩余时间为: 1.5
> task3 start
> task3 end
  • 分析 如果我们将 sleep 改为10s,那对应的整个过程就持续30s(差不多)
    但有一个现象是,并不是完整的按照 10s 10s 10s的停顿打印,可能会出现 20s 10s这种打印。仿佛将控制权交给浏览器后,浏览器还没来得及输出,就进行了下一次IdleCallback
    那这个10s后交出控制权的意义在哪儿呢?既然打印都如此不遵守时间的话
    答案是在事件响应、绘制等任务上。也就是说,在这30s期间,你尝试滚动鼠标(或者输出等其他事件),只有中间的两次交出控制权的间隙是成功滚动的,其他时间滚动都是无效的

另外通过这个例子我们可以看到,一旦idle任务开始执行,这个任务如果时间过长,超过了一帧的时间,由于此时主动权没有交还给浏览器,所以浏览器没办法进行渲染、响应输入等其他动作,此时就会出现"卡顿"现象

相关推荐
J不A秃V头A30 分钟前
Vue3:编写一个插件(进阶)
前端·vue.js
光影少年42 分钟前
usemeno和usecallback区别及使用场景
react.js
司篂篂1 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客1 小时前
pinia在vue3中的使用
前端·javascript·vue.js
宇文仲竹2 小时前
edge 插件 iframe 读取
前端·edge
Kika写代码2 小时前
【基于轻量型架构的WEB开发】【章节作业】
前端·oracle·架构
天下无贼!3 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
Jiaberrr3 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选
赵啸林3 小时前
npm发布插件超级简单版
前端·npm·node.js
罔闻_spider4 小时前
爬虫----webpack
前端·爬虫·webpack