走进浏览器之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:拿起水,喝~
- 我订了个午餐 2:上面预计送达时间 1 分钟,我当然不可能坐等外卖!
- 我订要喝水 3:拿起水,喝~
- 我定了个水果 4:上面预计送达时间 0,但只要是外卖,我就不着急!
- 订完外卖,我直接喝水 5:嗝~,不能再喝了,该谢谢了
- 此时我正好没事做,不如去看看外卖?
- 果然预计时间 0s 送达的水果先到了,我先吃口水果
- 吃完水果午餐也到了,嗯,愉快的一天
需要注意的是:我们这样对待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>
标签包裹的整体代码,是属于宏任务。
- 任务会依次进入执行栈,而首先入场的就是——宏任务全局上下文(
script
); - 执行同步任务;
- js 引擎遇到一个异步任务后并不会一直等待其返回结果:
- 遇到异步任务,交给异步处理模块处理,对应的异步处理线程处理异步任务需要的操作,例如定时器的计数和异步请求监听状态的变更;
- 当异步事件返回结果后,事件触发线程将回调函数(标记)加入任务队列,等待栈为空时,依次进入栈中执行;
- 执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空:
- 先检查微任务(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,
- 遇到宏任务
setTimeout-log(2)
,交给异步处理模块 - 遇到宏任务
setTimeout-log(3)
,交给异步处理模块 - 顺序执行,打印日志 4,
- 同步任务执行完毕,读取微任务队列,为 null,则读取宏任务队列第一个,先返回处理结果的是
setTimeout-log(3)
,将 log(3) 内回调函数加入执行队列,顺序执行,打印日志 3, - 继续重复步骤 5,顺序执行,打印日志 2,执行结束。
注意:
读取宏任务队列的任务,setTimeout-log(3)
,虽然定时器的等待时间被设置 0 秒,但是W3C
在HTML标准
中规定,规定要求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);
第一轮循环
- 同样从全局任务入口,遇到宏任务
setTimeout-log(1)
,交给异步处理模块,由于等待时间为 0,直接加入宏任务队列。 - 再次遇到宏任务
setTimeout-log(3)
,交给异步处理模块,同样直接加入宏任务队列。 - 遇到微任务
then()-log(4)
,加入微任务队列。 - 最后遇到打印语句,直接打印5。
- 执行栈空
画出当前任务队列的情况:
第二轮循环
- 执行栈空后,读取微任务队列,执行
then()-log(4)
,打印4,微任务队列为null;
画出此刻任务队列的情况:
- 微任务队列为空,读取宏任务队列的最靠前的任务
setTimeout-log(1)
;; - 打印1,又遇到微任务
then()-log(2)
,加入微任务队列。第二轮循环结束。
画出当前任务队列的情况:
第三轮循环
- 读取微任务队列:
then()-log(2)
,打印2,微任务队列为null - 读取宏任务队列首个任务:
setTimeout()-log(3)
,打印3
至此,异步任务队列也被清空,整个任务执行完毕
最终结果:5,4,1,2,3
五、写在最后
尝试走进浏览器的大门,你会对自己天天要调试的页面(浏览器)变得越来越亲切,也许有一天你会深耕在浏览器领域,希望而这一切的起因都是因为这个浏览器系列,来自《余光前端笔记》
5.1 本文参考
5.2 关于我
关于我
- 花名:余光
- WX:j565017805
- 邮箱:webbj97@163.com
其他沉淀
以上是关于走进浏览器之Event Loop的那些事的主要内容,如果未能解决你的问题,请参考以下文章
[未解决问题记录]python asyncio+aiohttp出现Exception ignored:RuntimeError('Event loop is closed')(代码片段