引用B站视频,搜索标题:【事件循环】【前端】事件原理讲解,超级硬核,忍不住转载
本视频总结:
超级复杂的JS底层。事件循环和事件队列的关系。宏任务、微任务和raf回调这3个事件队列的关系。任务队列和执行栈的关系。dom点击事件和js调用函数对执行栈的不同影响。事件循环和 dom渲染之家raf回调函数的执行。附带要理解函数调用过程词法环境和执行上下文。
如果有哪些不清楚的,可以先看----------------------------《〈《〈《以下部分》〉》〉-----------------------------------------
关键词:阻塞,主线程,事件环(event loop)、任务环(任务队列)、单线程、
1、浏览器的四大进程
2、消息队列和事件循环:页面是怎么活起来的
3、消息队列、延时队列(进程)、事件循环、微任务的产生
4、RAF是什么东东
其他文章中指的 (消息队列 === 任务队列,队列中存放的是一些线程(事件触发、定时器、http等线程))事件环(event loop)会取队列中的第一个先执行,符合先进先出的原则,,
-------------------------------------------《〈《〈《以上部分》〉》〉-----------------------------------------
弹幕上说碳酸饮料对课程没有关系,其实他是在类比举例。可以这么理解,浏览器维护了一个队列,里面放着一大群喝碳酸水的人(task),这些人很古怪,需要尽量减少和他们打交道(微任务难以控制,所以后面推荐用requestAnimation去代替setTimeout)。
至于后面那个高层就是纯粹活跃气氛了,这么看应该能理解他为什么举例喝碳酸水了。我感觉这就是文化差异吧,可能咱们这么演讲的太少了。顺便一提,他之前说他
女朋友提醒他还有几天去新加坡时也是一个类比,和setTimeout差不多一个样子
javascript --》 浏览器 --》 事件环
一、事件环跟任务队列
1、什么是事件环(event loop)??
在程序执行的过程中都有主线程,允许有其他的线程进入,比如网页(浏览器),一旦这些线程需要页面响应操作,需要通知主线程,就需要事件环来协调工作。
2、那必须先解释什么是主线程??
主线程 – 有大量的事情发生 1、javascript 发生的地方。 2、渲染发生的地方。 3、DOM存放的地方。
这也说明网页上大部分活动都具有确定的顺序,我们不会同时运行多段代码去修改同一处DOM
2.1 人们会什么不会思考单线程?
作为人类,我们并没有主线程,而人类就是多线程的。
人 ( 醒着 )的时候是— 多线程, 可以说话,看,听,脚动
人 ( 睡觉 )的时候是— 单线程,看不见,听不到,只是在睡觉
在程序执行的过程中都有主线程,允许有其他的线程进入,比如网页(浏览器),一旦这些线程需要页面响应操作,需要通知主线程,就需要事件环来协调工作。
2.2 任务队列,也是任务环中最重要的一部分。
setTimeout(() => {
如果页面中所有内容都放在延时器中,会阻塞页面执行
}, 5000)
setTimeout(() => {
console.log(‘时间到了,开始执行!!!’)
}, 5000)
如果页面中所有内容都放在延时器(以外),页面就会正常执行,将延时器添加到任务队列中,当到5s时间的时候,会打开与主线程的开关,执行。
3、事件环正常运行时
4、事件环中有任务队列时
任务队列:嘿,我有个任务交给你
主线程:
任务循环中首先要关注的 TASK Queues,先有事件环,然后
浏览器:有新的事件会通知到事件环
事件环说:已经将它放在待办列表,稍后就会执行,
如下两个setTimeout,事件环应该怎么执行。
setTimeout(() => {console.log('111111')}, 1000)setTimeout(() => {console.log('22222')}, 1000)
事件环中添加任务队列(如上图)
进入任务队列,开始执行第一个setTimeout
开始执行任务队列中的第二个setTimeout,执行完之后关闭任务队列的通道
事件环、任务队列的内容就分享完了,如果不是很懂,那就研究 Promise吧
点击跳转promise文章地址
二、事件环、任务队列、渲染就变的复杂了
当考虑到渲染时就变的复杂了
布局树、绘制图层
1、执行javascript脚本
2、计算界面元素的css样式
3、重新计算界面元素的布局
4、开始开始绘制界面
5、合成层(如果有需要的话)
在javascript中执行以下代码:
setTimeout(() => {while(true)})
当遇到死循环后,事件循环卡在了任务队列,需要等这个任务执行完之后,再绘制页面
当事件环在死循环的过程中,用户点击按钮,复制文字,都会将这一系列事件放在任务队列中,等待事件循环执行完成死循环在继续执行
Promise.resolve().then(() => {console.log(1);Promise.resolve().then(() => console.log(3)).then(() => console.log(4));}).then(() => console.log(2));
//上面代码打印出来的顺序是: 1 3 2 4。为什么是这样?一直没搞懂。
以上图解就是 while为什么会阻止渲染和其他页面交互,这其实是一件好事
比如同样是动画,用 animation 执行,就是匀速的
而使用 settimeout执行,就是闪动走的, 因为页面渲染跟 60fps(屏幕刷新率)有关,假设为 60fps,那么 settimeout(() => {}, 1000/60 = 16.6ms为一帧(frame)),就是当屏幕刚好刷新时,会记录下闪动的位置。不清楚可看RAF是什么。
RAF总结,如果不清楚,看最后介绍RAF是什么。
帧和动画
假如人的眼睛 1秒看60张图片, 那么动画最低的是60帧,才会认为这个动画是流畅的,
1s 60张图片 动画最低60帧,
1s = 1000ms / 60帧 = 16.6ms ,1帧 === 16.6ms,非常快
渲染流水线
document.getElementById(“box”).style = “height: 100px”;
假如修改样式需要 1ms, 但是每一帧 16.6ms,不可能直接渲染,需要等到这一帧,
s,l,p
Javascript操作dom时产生的变化是需要浏览器执行界面的绘制任务才能被更新到屏幕上的,在事件循环中,我们依次将这些任务入队,然后执行。但是我们之前提到过,浏览器的刷新率是60fps,也就是说,后面三步并不是总是接着javascript脚本的执行而执行的,而是需要等待屏幕刷新之前执行这三个任务
延时器
最小间隔时间是4.7ms,4.7 * 4(延时器) = 18.8 === 1帧(16.6)在执行了3-4次定时器函数之后,我们才能看到一个片段被绘制在屏幕上存在的问题:1、不准确2、第一个如果延迟(http请求),会影响下一帧的渲染3、流畅度,速率,
RAF (requestAnimationFrame)浏览器自己的,每一帧(16.6ms)
注意: requestAnimationFrame 回调函数运行在处理CSS和绘制之前(当渲染时才会看(16.6ms)),并且raf的执行速率与屏幕刷新的速率相同,
假设一:、我们点击按钮, 让div先移动 1000px,再往回移动500px
button.addEventListener('click',() => {box.style.transform 'translateX(1000px)';box.style.transition 'transform 1s ease-in-out';requestAnimationFrame(()=>box.style.transform 'translateX(500px)';});});
解析:
javascript 开始执行, 发现 以下代码,需要1msbox.style.transform 'translateX(1000px)';box.style.transition 'transform 1s ease-in-out';程序中发现有动画帧requestAnimationFrame,等执行到16.6ms(1帧)的时候,box.style.transform 'translateX(500px)';
然后页面开始 提取style、layout,paint,最后渲染的就是移动的 500;
假设二、 requestAnimationFrame嵌套requestAnimationFrame
button.addEventListener('click',() => {box.style.transform 'translateX(1000px)';box.style.transition 'transform 1s ease-in-out';requestAnimationFrame(() => {requestAnimationFrame(()=>box.style.transform 'translateX(500px)';});})});
解析:
javascript 开始执行, 发现 以下代码,需要1ms,发现动画帧(requestAnimationFrame),div开始渲染 style,layout,paint,执行这三步以后div此时移动了 1000px,requestAnimationFrame 回调函数运行在处理CSS和绘制之前(当渲染时才会看(16.6ms))javascript 执行中发现了第二帧(requestAnimationFrame),div开始渲染 style,layout,paint,box.style.transform 'translateX(500px)';最后又往回移动了 500px
RAF是什么东东
开篇
我们之前在这篇文章里面讲过浏览器的事件循环,还提到事件队列,调用栈等浏览器的一些实现机制。但还有一些细节我们没有提到,这篇文章我们就来把这些细节补充。
帧和动画
你一定知道动画片是怎样制作的,没错,只需要很多张画满动画的纸张,只要这些纸张的动画情景是按照时间的连续性排列,那么他们按照一定的速度在你的眼前切换,你就能看到一部完整的动画片了。我们的电脑播放的各种操作动画也是一样的道理。你端坐在电脑面前的时候,gpu就是不断地在屏幕上绘制图片才会让你觉得电脑真的是在动起来了。人的眼睛如果在1秒之内看到有超过60张图片在切换,那么我们就相当于看到了一部动画一样。在能看到动画最低的动画是60帧,就是1面内60次的连续动作,我们会认为这个动画是流畅的。按照计算我们可以得出1000/60 = 16.6ms为一帧(frame)。大多数电脑也是按照这个速率刷新我们的屏幕。
渲染流水线
和电脑一样,浏览器也是按照这个频率把网页上的元素的变化反馈给GPU的。如果屏幕的刷新率60fps,那么我们的js脚本执行一次我们就是重绘一次界面会不会太浪费了,因为js脚本的执行大多数时候非常短暂。为了避免这种不必要的浪费,我们的浏览器是按照电脑刷新频率执行绘制的界面的任务。也就是说假设我们改变dom的脚本时间只用了1ms,那么需要等待一段时间10~15ms才会执行我们的绘制界面任务工作。这期间消息队列中会是空的,不会有渲染相关的任务被推入消息队列被执行。我们操作dom的场景一般如下代码所示:
document.getElementById("box").style = "height: 100px";
这段代码修改了界面上一个id为box的样式,我们虽然只执行了一条简短的语句,但是浏览器却为我们做了很多事情:
- 执行javascript脚本
- 计算界面元素的css样式
- 重新计算界面元素的布局
- 开始开始绘制界面
- 合成层(如果有需要的话)
Javascript操作dom时产生的变化是需要浏览器执行界面的绘制任务才能被更新到屏幕上的,在事件循环中,我们依次将这些任务入队,然后执行。但是我们之前提到过,浏览器的刷新率是60fps,也就是说,后面三步并不是总是接着javascript脚本的执行而执行的,而是需要等待屏幕刷新之前执行这三个任务。一般来说这个时间是大概是16.6ms,也就是屏幕刷新率60fps。所以,我们的脚本执行如果非常快的话,那么操作结果就会“等待”屏幕刷新才会被用户看到,这个等待时间非常短,对于人的眼睛来说完全不会有延迟,但是对于高速运转的计算机,可以节约很多绘制界面的任务和资源。
定时器
使用settimeout或者setinterval来渲染动画存在一些问题,首先就是他们最小间隔时间是4.7ms,而并非是你指定的0,所以当你用settimeout 0 来执行你的动画,你会发现实际上移动的速度是要比预期的执行速度快的。我们之前说过,界面绘制的速率是16.6ms左右,而定时器最小执行时间是4.7ms,所以,在执行了3-4次定时器函数之后,我们才能看到一个片段被绘制在屏幕上,正确的应该是给定时器设置16.6ms的时间,才正好与屏幕刷新率同步。
16.6ms这个时间只是我们推算出来的一个事件,定时器对这个时间只需并不准确,而却随着执行次数的推一,误差会越来越远离实际值
使用定时器运行动画函数的另外一个缺点就是settimeout在每一帧的执行时间会受到其他任务和自身的影响,这种影响如果叠加,会影响到定时器在下一帧出现的位置。为了形象地说明,我们来看下面这张图。
从上面的图来看,我们虽然可以确保定时器执行的间隔的时间,但是无法确定定时器执行时所在一帧的位置,而且在某些情况下,过长的执行时间会导致后面的绘制任务被推后,从而影响动画执行的流程度。
总得来说使用定时器来执行动画有以下几种缺点:
- 定时器的计算过大会影响动画的流畅度,而过小则影响动画的速率。
- 定时器的延迟时间其实是并不精确,并且随着时间推移精度会逐渐下降。
- 定时器的执行时机会被其他长任务影响,所以并不好准确控制每一帧的开始位置。
requestAnimationFrame
raf之所以会适合做动画效果,其中一个很大的原因就是raf的执行速率与屏幕刷新的速率相同。浏览器会在下一次界面绘制之前执行raf函数。这个时间并不需要我们自己去指定,浏览器已经自己定义好了。我们可以看下面的图:
可以看到,左边是js脚本的循环赛道,右边是raf、渲染流水线的赛道。左边任务执行的频率是实时的,也就是说一旦有任务被推入了消息队列,立马执行,而右边则是有规律的执行,这个规律则是屏幕刷新律,也就是大概16.6ms就执行一次。所以在rAF中的代码,执行速率是与定时器中的不一样的。我们以一下代码为例:
el.style.display = "block";
el.style.display = "none";
el.style.display = "block";
el.style.display = "none";
el.style.display = "block";
如果你解了刷新原理,就能很好的回答上面的问题,由于js执行的非常快,上面的语句几乎可以在1ms内执行完成,但是我们的绘制任务需要等待下一次屏幕刷新之前才能执行,因此我们只会看到最后一条js执行的结果,实际上界面不会有任何变化。
使用raf的另外一个好处就是因为raf处在每一帧的最前面,所以有足够多的剩余时间去执行自身或者其他计算绘制的任务,保证所有的任务都在一帧内被执行,如图所示:
与其他大多数浏览器不一样,苹果系统在处理raf函数时讲这个函数执行顺序放到了刷新界面之后,导致的问题就是有一帧无法被观察到。
总结
这次我们介绍了定时器以及rAF函数,他们的工作原理以及实现动画的优先选项。其实如果你深入了解过浏览器的一些渲染机制,就能很好地理解定时器和rAF函数的区别,以及为什么我们会优先选择后者作为动画的执行的函数。不仅仅在dom上,我们在处理2d或者3d动画的时候,都是采用的rAF函数让计算机去计算每个物体的位置变化。在处理大数据视觉上我们更需要关心的性能,定时器显然跟不上我们的需求。
参考文档:
-Philip Roberts
这个博主很有趣,其他文章参考链接
参考链接:RAF是什么东东
微任务
现在我们知道了消息队列,事件循环以及调用栈这些概念,我们才好继续理解微任务。为什么我们有了宏任务,还需要微任务呢?
早期的浏览器并没有区分宏任务和微任务,所有的任务统一都是宏任务。但是随着浏览器的发展,很多业务的复杂度上升,对性能就有所要求。但是如果假定任务数量不变,我们是在本质上是无法做到减少时间的,因此我们就需要将某一些优先任务进行细分,对不同的任务进行优先级排队。
优先任务:在一个网页时,dom操作和用户交互优先程度是最高的,这样才不会让用户有卡顿的感觉,因此,我们把dom变化作为一个优先任务考虑。
我们来举一个例子,来说明为什么需要微任务。早期的浏览器为了监听dom的变化,我们有两种方式
- 用setTimeout轮训,判断元素是否变化。
- 使用Mutation Event,判断元素的变化。
这两个方法都有各自的缺点,第一种我们无法判断dom变化的速率,如果间隔时间设置过快,毫无以为会浪费性能;而如果过慢则无法实时监听到dom的变化。而第二种虽然采用了异步的方式监听dom的变化,但是没有解决如果前面的任务执行过久的问题。而且dom的频繁变动会造成大量频繁的操作。为了解决这些问题,浏览器映入了映入了一个新的api:MutationObserver来监听dom变化,把以上两个问题都解决,第一,利用微任务将dom处理的优先级提升,第二,一次性收集多个dom变化一起处理。现在我们就来看看,浏览器是如何提升微任务的执行优先级的呢?我用下面的一张图来做说明:
消息队列中有很多个宏任务等待被执行,然后每个宏任务的队尾都有一个微任务队列,当执行某个宏任务的过程中有微任务(如MutationObserver监听到的dom变化,promise.resolve等)v8会把产生的任务加入到当前宏任务的微任务队列中,当这个宏任务执行完成,v8会去检查当前的任务的微任务队列是否为空(我们称这个时间点为检查点checpoint),如果为空,则继续下一个宏任务,如果不为空则去执行对应的微任务。可以想见,如果没有微任务的这种机制,那么我们新产生的任务就会被派到消息队列的最顶部分,等待其他的宏任务完成,再执行这些变化,这毫无疑问会影响dom改变的时间,从影响到客户的体验。
每个宏任务的队尾都有一个微任务队列
setTimeout(() => {new Promise((resolve) => {resolve('微任务1‘)}).then((respon) => { console.log('111', respon)})})setTimeout(() => { console.log('11111')})setTimeout(() => {new Promise((resolve) => {resolve('微任务2‘)}).then((respon) => { console.log('222', respon)})})
微任务队列:[] 想当于一个数组,每次有新的微任务先push队列中如果微任务中产生了新的微任务,那么下一个宏任务依旧要等待这个微任务被执行完成。
浏览器中哪些操作会产生微任务呢?
1.MutationObserver监听的dom变化时会回调函数会被作为微任务处理,因为dom的变化响应要非常及时,不能被其他的宏任务插队。
2.Promise.reslove也会产生微任务,详情我在之前的博文中已经提到过,有兴趣的可以过去查看。