再谈谈 Promise, setTimeout, rAF, rIC

欢迎关注我的公众号睿Talk,获取我最新的文章:
clipboard.png

一、前言

Promise, setTimeout, requestAnimationFrame, requestIdleCallback 这几个概念相信很多人都很熟悉了,最近在看 React Fiber 源码的时候又对它们有了更深一层的认识,在此分享一下。下文将用 rAF 代表 requestAnimationFrame, rIC 代表 requestIdleCallback

二、事件循环与帧

事件循环和上面 4 个名词的基本概念在此不再啰嗦了,我们着重看下它们之间的关系。浏览器是一个 UI 系统,所有的操作最终都会以页面的形式展现,而页面的基本单位是帧。一帧中可能包括的任务有下面几种类型。

clipboard.png

  • events: 点击事件、键盘事件、滚动事件等
  • macro: 宏任务,如 setTimeout
  • micro: 微任务,如 Promise
  • rAF: requestAnimationFrame
  • Layout: CSS 计算,页面布局
  • Paint: 页面绘制
  • rIC: requestIdleCallback

理想情况下,页面会以 60 帧每秒的帧率来运行,但实际上每秒绘制多少帧是由多个因素决定的,下面举一些例子:

  • 一个加载完成的静态页面,当用户没有进行交互的情况下,页面不需要重绘,帧率为 0。
  • 快速滚动页面的时候,可视区域的内容不断发生变化,浏览器会尽可能快的重绘页面,理想帧率为 60。
  • 假设页面有一个注册了回调的按钮,回调执行需要 500 毫秒。当点击按钮后再快速滚动页面,头 500 毫秒页面是卡住动不了的,后 500 毫秒会尽可能快的重绘页面,这时候理想帧率为 30。
  • 当使用 rAF 制作动画的时候,浏览器会尽可能快的重绘页面,桌面浏览器可能是 60 帧,移动浏览器可能是 30 帧。

从上面的例子可以看出,页面的帧率不是固定的,是会动态变化的。当某一帧的任务占用大量时间的时候,会影响到下一帧的执行。那么谁来调节帧率呢?显然只能依靠浏览器自身。作为开发者的我们是无法准确预知回调什么时候执行的。比如:

function animation() {
   console.log('time: ', +new Date());
   setTimeout(animate, 1000 / 60);
}

animation();

上面的函数假定了浏览器以帧率 60 来运行,但当帧率达不到的时候,2 帧之间回调可能执行了多次,也可能一次都不执行,简称掉帧。

所以在制作动画的时候,我们不能预设浏览器的帧率,正确的做法是通过 rAF 注册回调, 由浏览器来控制动画调用时机:

function animation() {
   console.log('time: ', +new Date());
   requestAnimationFrame(animation);
}

animation();

rAF 会保证注册的回调在下次渲染页面之前执行,且只会执行一次。另外,当页面处于不可见状态时,rAF 会自动停止执行,以节省系统资源。

三、执行顺序

Promise, setTimeout , rAFrIC 对应 4 种队列:微任务队列、宏任务队列、animation 队列和 idle 队列。

  • 微任务队列会在 JS 运行栈为空的时候立即执行。
  • animation 队列会在页面渲染前执行。
  • 宏任务队列优先级低于微任务队列,一般也会比 animation 队列优先级低,但不是绝对
  • idle 队列优先级最低,当浏览器有空闲时间的时候才会执行。
setTimeout(()=>console.log('setTimeout'), 0);
Promise.resolve().then(()=>console.log('promise'));
requestAnimationFrame(()=>console.log('animation'));
requestIdleCallback(()=>console.log('idle'));

// 执行结果大多数情况下是: promise, animation, setTimeout, idle
// 少数情况是:promise, setTimeout, animation, idle

再来谈谈空闲时间怎么理解。假设在 1 秒内有 3 帧需要渲染:

clipboard.png

  • 第一帧,由于宏任务占用了大量的时间,没有空闲时间。
  • 第二帧,rAF占用的时间不多,有大量的空闲时间
  • 第三帧,浏览器事件占用的时间不多,有大量的空闲时间

rAF类似,rIC 的执行时机是由浏览器控制的,能更好的保证体验,优化性能。一般优先级高的任务(如 UI 更新)会放在 rAF 队列,优先级低的任务(如日志上传)会放 rIC

四、队列特性

在一个事件循环内,各个队列有以下特性:

  • 宏任务队列,每次只会执行队列内的一个任务。
  • 微任务队列,每次会执行队列里的全部任务。假设微任务队列内有 100 个 Promise,它们会一次过全部执行完。这种情况下极有可能会导致页面卡顿。如果在微任务执行过程中继续往微任务队列中添加任务,新添加的任务也会在当前事件循环中执行,很容易造成死循环, 如:
function loop() {
    Promise.resolve().then(loop);
}

loop();
  • animation 队列,跟微任务队列有点相似,每次会执行队列里的全部任务。但如果在执行过程中往队列中添加新的任务,新的任务不会在当前事件循环中执行,而是在下次事件循环中执行。
  • idle 队列,每次只会执行一个任务。任务完成后会检查是否还有空闲时间,有的话会继续执行下一个任务,没有则等到下次有空闲时间再执行。需要注意的是此队列中的任务也有可能阻塞页面,当空闲时间用完后任务不会主动退出。如果任务占用时间较长,一般会将任务拆分成多个阶段,执行完一个阶段后检查还有没有空闲时间,有则继续,无则注册一个新的 idle 队列任务,然后退出当前任务。React Fiber 就是用这个机制。但最新版的 React Fiber 已经不用 rIC 了,因为调用的频率太低,改用 rAF

五、总结

本文介绍了 4 种队列的执行顺序和每个队列的特性,它们是:宏任务队列、微任务队列、animation 队列和 idle 队列。实际应用时可以根据它们各自的特点分配不同的任务。


睿Talk
记录个人和团队技术成长的点点滴滴
5.5k 声望
422 粉丝
0 条评论
推荐阅读
Web前端主题切换的几种方案
这种方案利用了css多层样式精确匹配的特点,通过样式覆盖的方式实现主题的切换。首先需要在应用的根元素中设一个 class,切换主题时给 class 赋上对应的值,下面以theme1/theme2为例。

Dickens8阅读 8.3k

封面图
ESlint + Stylelint + VSCode自动格式化代码(2023)
安装插件 ESLint,然后 File -> Preference-> Settings(如果装了中文插件包应该是 文件 -> 选项 -> 设置),搜索 eslint,点击 Edit in setting.json

谭光志34阅读 20.8k评论 9

安全地在前后端之间传输数据 - 「3」真的安全吗?
在「2」注册和登录示例中,我们通过非对称加密算法实现了浏览器和 Web 服务器之间的安全传输。看起来一切都很美好,但是危险就在哪里,有些人发现了,有些人嗅到了,更多人却浑然不知。就像是给门上了把好锁,还...

边城32阅读 7.3k评论 5

封面图
涨姿势了,有意思的气泡 Loading 效果
今日,群友提问,如何实现这么一个 Loading 效果:这个确实有点意思,但是这是 CSS 能够完成的?没错,这个效果中的核心气泡效果,其实借助 CSS 中的滤镜,能够比较轻松的实现,就是所需的元素可能多点。参考我们...

chokcoco24阅读 2.2k评论 3

在前端使用 JS 进行分类汇总
最近遇到一些同学在问 JS 中进行数据统计的问题。虽然数据统计一般会在数据库中进行,但是后端遇到需要使用程序来进行统计的情况也非常多。.NET 就为了对内存数据和数据库数据进行统一地数据处理,发明了 LINQ (L...

边城17阅读 2k

封面图
过滤/筛选树节点
又是树,是我跟树杠上了吗?—— 不,是树的问题太多了!🔗 相关文章推荐:使用递归遍历并转换树形数据(以 TypeScript 为例)从列表生成树 (JavaScript/TypeScript) 过滤和筛选是一个意思,都是 filter。对于列表来...

边城18阅读 7.8k评论 3

封面图
Vue2 导出excel
2020-07-15更新 excel导出安装 {代码...} src文件夹下新建一个libs文件夹,新建一个excel.js {代码...} vue页面中使用 {代码...} ===========================以下为早期的文章今天在开发的过程中需要做一个Vue的...

原谅我一生不羁放歌搞文艺14阅读 20k评论 9

5.5k 声望
422 粉丝
宣传栏