摘要:二事件循环与帧事件循环和上面个名词的基本概念在此不再啰嗦了,我们着重看下它们之间的关系。浏览器是一个系统,所有的操作最终都会以页面的形式展现,而页面的基本单位是帧。当某一帧的任务占用大量时间的时候,会影响到下一帧的执行。
欢迎关注我的公众号睿Talk,获取我最新的文章:
Promise, setTimeout, requestAnimationFrame, requestIdleCallback 这几个概念相信很多人都很熟悉了,最近在看 React Fiber 源码的时候又对它们有了更深一层的认识,在此分享一下。下文将用 rAF 代表 requestAnimationFrame, rIC 代表 requestIdleCallback。
二、事件循环与帧事件循环和上面 4 个名词的基本概念在此不再啰嗦了,我们着重看下它们之间的关系。浏览器是一个 UI 系统,所有的操作最终都会以页面的形式展现,而页面的基本单位是帧。一帧中可能包括的任务有下面几种类型。
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 , rAF 和 rIC 对应 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 帧需要渲染:
第一帧,由于宏任务占用了大量的时间,没有空闲时间。
第二帧,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 队列。实际应用时可以根据它们各自的特点分配不同的任务。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/109452.html
摘要:比如下面一个例子例输出为先输出,没有问题,因为是同步任务在主线程中优先执行,这里的问题是和任务的执行优先级是如何定义的。 在原文的基础上加了一点参考资料 问题的引出 event loop都不陌生,是指主线程从任务队列中循环读取任务,比如 例1: setTimeout(function(){console.log(1)},0); console.log(2) //输出2,1 在上述...
摘要:回调函数这是异步编程最基本的方法。对象对象是工作组提出的一种规范,目的是为异步编程提供统一接口。诞生后,出现了函数,它将异步编程带入了一个全新的阶段。 更多详情点击http://blog.zhangbing.club/Ja... Javascript 语言的执行环境是单线程的,如果没有异步编程,根本没法用,非卡死不可。 为了解决这个问题,Javascript语言将任务的执行模式分成两种...
摘要:一般会这样去写要在第一个请求成功后才可以执行下一步这样的写法的原理是,当执行一些异步操作时,我们需要知道操作是否已经完成,所有当执行完成的时候会返回一个回调函数,表示操作已经完成。 前言 开篇首先设想一个日常开发常常会遇到的需求:在多个接口异步请求数据,然后利用这些数据来进行一系列的操作。一般会这样去写: $.ajax({ url: ......, success: f...
摘要:二浏览器端在讲解事件循环之前先谈谈中同步代码异步代码的执行流程。三端我自己认为的事件循环和浏览器端还是有点区别的,它的事件循环依靠引擎。四总结本篇主要介绍了浏览器和对于事件循环机制实现,由于能力水平有限,其中可能有误之处欢迎指出。 一、前言 前几天听公司一个公司三年的前端说今天又学到了一个知识点-微任务、宏任务,我问他这是什么东西,由于在吃饭他浅浅的说了下,当时没太理解就私下学习整理一...
摘要:异步问题回调地狱首先,我们来看下异步编程中最常见的一种问题,便是回调地狱。同时使用也是异步编程最基础和核心的一种解决思路。基于,目前也被广泛运用,其是异步编程的一种解决方案,比传统的回调函数解决方案更合理和强大。 关于 微信公众号:前端呼啦圈(Love-FED) 我的博客:劳卜的博客 知乎专栏:前端呼啦圈 前言 在实际编码中,我们经常会遇到Javascript代码异步执行的场景...
阅读 1962·2023-04-25 15:45
阅读 1196·2021-09-29 09:34
阅读 2496·2021-09-03 10:30
阅读 1999·2019-08-30 15:56
阅读 1454·2019-08-29 15:31
阅读 1266·2019-08-29 15:29
阅读 3194·2019-08-29 11:24
阅读 3046·2019-08-26 13:45