走进浏览器之Event Loop的那些事

Posted 余光、

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了走进浏览器之Event Loop的那些事相关的知识,希望对你有一定的参考价值。

走进 Event Loop

大家都知道,在使用 setTimeout 或是其他异步任务的时候要多加小心,这是因为其涉及到了 Js 的事件循环机制。很多时候因为经验,我们不需要知道其原理就能避免一些错误,但今天我们还是来聊一聊,浏览器下的EventLoop机制的那些事~

在这里插入图片描述

一、javascript 与单线程?

JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么 JavaScript 不能有多个线程呢?这样能提高效率啊。

答:

JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

二、任务

简单了解一下同步和异步这两个概念

2.1 同步与异步

一般而言,任务分为:发出调用和得到结果两步。

  • 发出调用,“立即”得到结果是为同步
  • 发出调用,但无法“立即”得到结果,需要额外的操作才能得到预期的结果是为异步。

同步任务需要就是调用之后一直等待,直到返回结果。异步则是调用之后,不能直接拿到结果,通过一系列的手段才最终拿到结果(调用之后,拿到结果中间的时间可以介入其他任务)。

来阅读下面的代码:

console.log(1);

setTimeout(() => {
  console.log(2);
}, 1000);

console.log(3);

setTimeout(() => {
  console.log(4);
}, 0);

console.log(5);

拟人分析:

  1. 我要喝水 1:拿起水,喝~
  2. 我订了个午餐 2:上面预计送达时间 1 分钟,我当然不可能坐等外卖!
  3. 我订要喝水 3:拿起水,喝~
  4. 我定了个水果 4:上面预计送达时间 0,但只要是外卖,我就不着急!
  5. 订完外卖,我直接喝水 5:嗝~,不能再喝了,该谢谢了
  6. 此时我正好没事做,不如去看看外卖?
  7. 果然预计时间 0s 送达的水果先到了,我先吃口水果
  8. 吃完水果午餐也到了,嗯,愉快的一天

需要注意的是:我们这样对待setTimeout(订外卖),是因为我们不知道要等多久,所以我们完全不关心他是不是马上送到,而是将眼前的任务(手里看得见的其他事情)执行完,当手里没事做时才会去执行setTimeout指定的回调函数(取外卖)。

这段分析合情合理,我感觉我离真相又进了一步!

2.2 宏任务与微任务

下面的两种任务,都属于异步任务,但浏览器对他们的处理存在很大的区别。

  • 宏任务:MacroTask
    • 如:setTimeout、setInterval、script(整体代码)、 I/O 操作、UI 渲染等。
  • 微任务:MicroTask
    • 如: new Promise().then(回调)、MutationObserver(html5 新特性) 等。

三、EventLoop

js 的另一大特点是非阻塞,借用书中的一句话:这需要一套机制来处理现在运行的事情和将来要运行的事情,而 EventLoop 就是这样一套机制。

3.1 执行栈

JS 执行栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。

3.2 对同步、异步的处理

Javascript 单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。

在这里插入图片描述

注意

  • 宏任务队列可以有多个;
  • 微任务队列只有一个;

3.3 EventLoop 过程解析

上图:

在这里插入图片描述

值得一提的是,整个<script>标签包裹的整体代码,是属于宏任务。

  1. 任务会依次进入执行栈,而首先入场的就是——宏任务全局上下文(script);
  2. 执行同步任务;
  3. js 引擎遇到一个异步任务后并不会一直等待其返回结果:
    • 遇到异步任务,交给异步处理模块处理,对应的异步处理线程处理异步任务需要的操作,例如定时器的计数和异步请求监听状态的变更;
    • 当异步事件返回结果后,事件触发线程将回调函数(标记)加入任务队列,等待栈为空时,依次进入栈中执行;
  4. 执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空:
    • 先检查微任务(microTask)队列,如果存在任务,则一次性执行完所有微任务,无任务则跳过
    • 后检查宏任务(macroTask)队列,如果存在任务,则取出第一个宏任务,执行,

总结 1:

每次单个宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为 null,然后再执行宏任务,如此循环。

总结 2:

又因为第一次进入执行栈的总是宏任务(Script),而每次宏任务完成后,都会读取微任务队列,所以,大家也会听见另一种表述方式:、

每一轮循环中,同步任务执行后会在末尾执行并清空所有的微任务,会在下一轮循环中取第一个宏任务执行,如此循环。

下面我们用两段代码来分析一下今天所学~

在这里插入图片描述

四、示例分析

4.1 同步+宏任务

console.log("1");
// 0s延迟的定时任务
setTimeout(function timeout() {
  console.log("2");
}, 1000);
setTimeout(function timeout() {
  console.log("3");
}, 0);
console.log("4");

以这段代码举例,我们按照第三小节的步骤逐步分析:

  1. 从全局任务入口,首先打印日志 1,
  2. 遇到宏任务setTimeout-log(2),交给异步处理模块
  3. 遇到宏任务setTimeout-log(3),交给异步处理模块
  4. 顺序执行,打印日志 4,
  5. 同步任务执行完毕,读取微任务队列,为 null,则读取宏任务队列第一个,先返回处理结果的是setTimeout-log(3),将 log(3) 内回调函数加入执行队列,顺序执行,打印日志 3,
  6. 继续重复步骤 5,顺序执行,打印日志 2,执行结束。

注意:

读取宏任务队列的任务,setTimeout-log(3),虽然定时器的等待时间被设置 0 秒,但是W3CHTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为 4ms。

由于浏览器在执行以上三步时,并未耗时很久,所以当宏任务setTimeout-log(3)1 执行完时, setTimeout-log(1)的等待时间并未结束,所以在 1 秒后打印,实际上并未等待 1 秒。

4.2 同步+宏任务+微任务

再来看下面的代码:

setTimeout(function() {
  console.log(1);

  Promise.resolve().then(function() {
    console.log(2);
  });
}, 0);

setTimeout(function() {
  console.log(3);
}, 0);

Promise.resolve().then(function() {
  console.log(4);
});

console.log(5);

第一轮循环

  1. 同样从全局任务入口,遇到宏任务setTimeout-log(1),交给异步处理模块,由于等待时间为 0,直接加入宏任务队列。
  2. 再次遇到宏任务setTimeout-log(3),交给异步处理模块,同样直接加入宏任务队列。
  3. 遇到微任务then()-log(4),加入微任务队列。
  4. 最后遇到打印语句,直接打印5
  5. 执行栈空

画出当前任务队列的情况:

第二轮循环

  1. 执行栈空后,读取微任务队列,执行then()-log(4)打印4,微任务队列为null;

画出此刻任务队列的情况:

  1. 微任务队列为空,读取宏任务队列的最靠前的任务setTimeout-log(1);;
  2. 打印1,又遇到微任务then()-log(2),加入微任务队列。第二轮循环结束。

画出当前任务队列的情况:

第三轮循环

  1. 读取微任务队列:then()-log(2)打印2,微任务队列为null
  2. 读取宏任务队列首个任务:setTimeout()-log(3)打印3

至此,异步任务队列也被清空,整个任务执行完毕

最终结果:5,4,1,2,3

五、写在最后

尝试走进浏览器的大门,你会对自己天天要调试的页面(浏览器)变得越来越亲切,也许有一天你会深耕在浏览器领域,希望而这一切的起因都是因为这个浏览器系列,来自《余光前端笔记》

5.1 本文参考

5.2 关于我

关于我

  • 花名:余光
  • WX:j565017805
  • 邮箱:webbj97@163.com

其他沉淀

以上是关于走进浏览器之Event Loop的那些事的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript之彻底学会Event Loop

JavaScript之彻底学会Event Loop

[未解决问题记录]python asyncio+aiohttp出现Exception ignored:RuntimeError('Event loop is closed')(代码片段

js中的event loop之浏览器篇

Webstorm那些事 之 调试(Debug)前端代码

和浏览器异步请求取消相关的那些事