二、浏览器--事件循环(也叫事件环,也叫event loop)--任务队列(等待执行的任务(存放的定时器,http,事件等进程))--渲染三者的关系

引用B站视频,搜索标题:【事件循环】【前端】事件原理讲解,超级硬核,忍不住转载

本视频总结,带着问题看文档:

  • 超级复杂的JS底层
  • 事件循环事件队列的关系。
  • 宏任务、微任务raf回调这3个事件队列的关系。
  • 任务队列和执行栈的关系。
  • dom点击事件js调用函数对执行栈的不同影响。
  • 事件循环dom渲染之家raf回调函数的执行。
  • 附带要理解函数调用过程词法环境和执行上下文。
如果有哪些不清楚的,可以先看----------------------------《〈《〈《以下部分》〉》〉-----------------------------------------

关键词:阻塞,主线程,事件环(event loop)、任务环(任务队列)、单线程、

1、浏览器的四大进程
2、消息队列和事件循环:页面是怎么活起来的
3、消息队列、延时队列(进程)、事件循环、微任务的产生
4、RAF是什么东东

其他文章中指的 (消息队列(谷歌浏览器中定义是message queue) === 任务队列,队列中存放的是一些线程(用户交互、延迟器、http等线程))事件环(event loop)会取队列中的第一个先执行,符合先进先出的原则,,

-------------------------------------------《〈《〈《以上部分》〉》〉-----------------------------------------

❓任务有优先级吗?

任务没有优先级,在消息队列中先进先出,但消息队列是有优先级的根据 W3C 的最新解释:

  • 每个任务都有⼀个任务类型,同⼀个类型的任务必须在⼀个队列,不同类型的任务可以分属于不同的队列。
  • 在⼀次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执
    ⾏。
  • 浏览器必须准备好⼀个微队列,微队列中的任务优先所有其他任务执⾏
    https://html.spec.whatwg.org/multipage/webappapis.html#p
    erform-a-microtask-checkpoint

❓随着浏览器的复杂度急剧提升,W3C 不再使⽤宏队列的说法

在⽬前 chrome 的实现中,⾄少包含了下⾯的队列

  • 延时队列:⽤于存放计时器到达后的回调任务,优先级「中」
  • 交互队列:⽤于存放⽤户操作后产⽣的事件处理任务,优先级「⾼」
  • 微队列:⽤户存放需要最快执⾏的任务,优先级「最⾼」

❓⾯试题:如何理解 JS 的异步?

参考答案:

  • JS是⼀⻔单线程的语⾔,这是因为它运⾏在浏览器的渲染主线程中,⽽渲染主线程只有⼀个。
  • ⽽渲染主线程承担着诸多的⼯作,渲染⻚⾯、执⾏ JS 都在其中运⾏。
  • 如果使⽤同步的⽅式,就极有可能导致主线程产⽣阻塞,从⽽导致消息队列中的很多其他任务⽆法得到执⾏。这样⼀来,
  • ⼀⽅⾯会导致繁忙的主线程⽩⽩的消耗时间,
  • 另⼀⽅⾯导致⻚⾯⽆法及时更新,给⽤户造成卡死现象。
  • 所以浏览器采⽤异步的⽅式来避免。具体做法是当某些任务发⽣时,⽐如计时器、⽹络、事件监听,主线程将任务交给其他线程去处理,⾃身⽴即结束任务的执⾏,转⽽执⾏后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加⼊到消息队列的末尾排队,等待主线程调度执⾏。在这种异步模式下,浏览器永不阻塞,从⽽最⼤限度的保证了单线程的流畅运⾏。

事件循环开始

浏览器的进程模型

1、何为进程?

  • 程序运⾏需要有它⾃⼰专属的内存空间,可以把这块内存空间简单的理解为进程
  • 每个应⽤⾄少有⼀个进程,进程之间相互独⽴,即使要通信,也需要双⽅同意。
    在这里插入图片描述

2、何为线程?

  • 有了进程后,就可以运⾏程序的代码了。
  • 运⾏代码的「⼈」称之为「线程」。
  • ⼀个进程⾄少有⼀个线程,所以在进程开启后会⾃动创建⼀个线程来运⾏代码,该线程称之为主线程。
  • 如果程序需要同时执⾏多块代码,主线程就会启动更多的线程来执⾏代码,所以⼀个进程中可以包含多个线程。

在这里插入图片描述

3、浏览器有哪些进程和线程?

  • 浏览器是⼀个多进程多线程的应⽤程序
  • 浏览器内部⼯作极其复杂。
  • 为了避免相互影响,为了减少连环崩溃的⼏率,当启动浏览器后,它会⾃动启动多个进程
    在这里插入图片描述

可以在浏览器的任务管理器中查看当前的所有进程:浏览器更多选项–> 更多工具–> 任务管理器

  • 其中,最主要的进程有:
    1. 浏览器进程
      主要负责界⾯显示、⽤户交互(事件点击)、⼦进程管理等。浏览器进程内部会启动多个线程处理不同的任务。
    2. ⽹络进程
      负责加载⽹络资源。⽹络进程内部会启动多个线程来处理不同的⽹络任务。
    3. 渲染进程(本节课重点讲解的进程)
      渲染进程启动后,会开启⼀个渲染主线程,主线程负责执⾏ HTML、CSS、JS 代码。默认情况下,浏览器会为每个标签⻚开启⼀个新的渲染进程,以保证不同的标签⻚之间不相互影响

4、渲染主线程是如何⼯作的?

渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:

  • 解析 HTML
  • 解析 CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把⻚⾯画 60 次(1000ms / 60 = 16.6 为一帧)
  • 执⾏全局 JS 代码
  • 执⾏事件处理函数
  • 执⾏计时器的回调函数
    在这里插入图片描述
    在这里插入图片描述
    1. 在最开始的时候,渲染主线程会进⼊⼀个⽆限循环
    1. 每⼀次循环会检查消息队列中是否有任务存在。如果有,就取出第⼀个任务执⾏,执⾏完⼀个后进⼊下⼀次循环;如果没有,则进⼊休眠状态。
    1. 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务
  • 这样⼀来,就可以让每个任务有条不紊的、持续的进⾏下去了。

何为异步?

代码在执⾏过程中,会遇到⼀些⽆法⽴即处理的任务,⽐如:

  • 计时完成后需要执⾏的任务 —— setTimeout 、 setInterval
  • ⽹络通信完成后需要执⾏的任务 – XHR 、 Fetch
  • ⽤户操作后需要执⾏的任务 – addEventListener
    如果让渲染主线程等待这些任务的时机达到,就会导致主线程⻓期处于「阻塞」的状态,从⽽导致浏览器「卡死」
    在这里插入图片描述
    在这里插入图片描述

整个过程,被称之为事件循环(消息循环)

5、什么是事件环(event loop / 或者说渲染主县城是如何工作的message queue)??

  • 事件循环⼜叫做消息循环,是浏览器渲染主线程的⼯作⽅式。
    在 Chrome 的源码中,它开启⼀个不会结束的 for 循环,每次循环从消息队列中取出第⼀个任务执⾏,⽽其他线程只需要在合适的时候将任务加⼊到队列末尾即可。
  • 过去把消息队列简单分为宏队列和微队列,这种说法⽬前已⽆法满⾜复杂的浏览器环境,取⽽代之的是⼀种更加灵活多变的处理⽅式。
  • 根据 W3C 官⽅的解释,每个任务有不同的类型,同类型的任务必须在同⼀个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在⼀次事件循环中,由浏览器⾃⾏决定取哪⼀个队列的任务。但浏览器必须有⼀个微队列,微队列的任务⼀定具有最⾼的优先级,必须优先调度执⾏。
5.1 人们会什么不会思考单线程?

作为人类,我们并没有主线程,而人类就是多线程的。

人 ( 醒着 )的时候是— 多线程, 可以说话,看,听,脚动

人 ( 睡觉 )的时候是— 单线程,看不见,听不到,只是在睡觉

在程序执行的过程中都有主线程,允许有其他的线程进入,比如网页(浏览器),一旦这些线程需要页面响应操作,需要通知主线程,就需要事件环来协调工作。

5.3、事件环正常运行时

在这里插入图片描述

6、事件环中有任务队列时

任务队列:嘿,我有个任务交给你
主线程:
任务循环中首先要关注的 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 开始执行, 发现 以下代码,需要1ms
  box.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样式
  • 重新计算界面元素的布局
  • 开始开始绘制界面
  • 合成层(如果有需要的话)

49.png

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在每一帧的执行时间会受到其他任务和自身的影响,这种影响如果叠加,会影响到定时器在下一帧出现的位置。为了形象地说明,我们来看下面这张图。
50.png

从上面的图来看,我们虽然可以确保定时器执行的间隔的时间,但是无法确定定时器执行时所在一帧的位置,而且在某些情况下,过长的执行时间会导致后面的绘制任务被推后,从而影响动画执行的流程度。

总得来说使用定时器来执行动画有以下几种缺点:
  1. 定时器的计算过大会影响动画的流畅度,而过小则影响动画的速率。
  2. 定时器的延迟时间其实是并不精确,并且随着时间推移精度会逐渐下降。
  3. 定时器的执行时机会被其他长任务影响,所以并不好准确控制每一帧的开始位置。

requestAnimationFrame

raf之所以会适合做动画效果,其中一个很大的原因就是raf的执行速率与屏幕刷新的速率相同。浏览器会在下一次界面绘制之前执行raf函数。这个时间并不需要我们自己去指定,浏览器已经自己定义好了。我们可以看下面的图:
52.png

可以看到,左边是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处在每一帧的最前面,所以有足够多的剩余时间去执行自身或者其他计算绘制的任务,保证所有的任务都在一帧内被执行,如图所示:
51.png

与其他大多数浏览器不一样,苹果系统在处理raf函数时讲这个函数执行顺序放到了刷新界面之后,导致的问题就是有一帧无法被观察到。

总结

这次我们介绍了定时器以及rAF函数,他们的工作原理以及实现动画的优先选项。其实如果你深入了解过浏览器的一些渲染机制,就能很好地理解定时器和rAF函数的区别,以及为什么我们会优先选择后者作为动画的执行的函数。不仅仅在dom上,我们在处理2d或者3d动画的时候,都是采用的rAF函数让计算机去计算每个物体的位置变化。在处理大数据视觉上我们更需要关心的性能,定时器显然跟不上我们的需求。

参考文档:
-Philip Roberts

这个博主很有趣,其他文章参考链接
参考链接:RAF是什么东东

微任务

现在我们知道了消息队列,事件循环以及调用栈这些概念,我们才好继续理解微任务。为什么我们有了宏任务,还需要微任务呢?
早期的浏览器并没有区分宏任务和微任务,所有的任务统一都是宏任务。但是随着浏览器的发展,很多业务的复杂度上升,对性能就有所要求。但是如果假定任务数量不变,我们是在本质上是无法做到减少时间的,因此我们就需要将某一些优先任务进行细分,对不同的任务进行优先级排队。

优先任务:在一个网页时,dom操作和用户交互优先程度是最高的,这样才不会让用户有卡顿的感觉,因此,我们把dom变化作为一个优先任务考虑。

我们来举一个例子,来说明为什么需要微任务。早期的浏览器为了监听dom的变化,我们有两种方式

  1. 用setTimeout轮训,判断元素是否变化。
  2. 使用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也会产生微任务,详情我在之前的博文中已经提到过,有兴趣的可以过去查看。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值