前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文带你搞懂浏览器的事件循环机制!

一文带你搞懂浏览器的事件循环机制!

作者头像
用户6297767
发布2023-11-21 08:47:25
3750
发布2023-11-21 08:47:25
举报

什么是事件循环

Event Loop 也叫做“事件循环”,它其实与 JavaScript 的运行机制有关,乍一看云里雾里,不用着急,读完本文你便会知晓它的含义,这一切都要从 JavaScript 的初始设计说起。

并发模型

JavaScript 的并发模型是基于事件循环机制的,这个机制被称为 Event Loop。它是一种单线程的执行模型,但是它可以通过异步编程来支持并发操作,从而实现高效的非阻塞 IO 操作。

在 JavaScript 中,所有的代码都是在同一个线程中执行的,这个线程被称为主线程或 UI 线程。当我们执行一段耗时较长的代码时,如果不采用异步编程的方式,那么这段代码将会阻塞主线程,导致整个应用程序变得不可响应。

为了避免这种情况,JavaScript 引入了异步编程的概念。异步编程使用回调函数、Promise、async/await 等方式来实现,它允许我们在主线程上同时处理多个任务,而不必等待任务完成。

在 JavaScript 中,异步任务通常被分为两类:宏任务和微任务 宏任务包括:

  • setTimeout
  • setInterval
  • I/O 操作等,

微任务则包括:

  • Promise
  • MutationObserver 等。

当主线程执行完当前的宏任务后,就会检查是否有微任务需要执行,如果有,则先执行微任务,然后再执行下一个宏任务。

JavaScript 的并发模型基于事件循环机制,它通过异步编程来实现高效的非阻塞 IO 操作。在 JavaScript 中,异步任务被分为宏任务和微任务,它们的执行顺序是由事件循环机制控制的。通过合理地使用异步编程,我们可以在单线程的 JavaScript 中实现高效的并发操作。

image.png
image.png

单线程

进程和线程是操作系统中的概念,在操作系统中,一个任务就是一个进程,比如你在电脑上打开了一个浏览器来观看视频,便是打开了一个浏览器进程,此时又想记录视频中的重要信息,于是你打开了备忘录,这便是一个备忘录进程,系统会为每个进程分配它所需要的地址空间,数据,代码等系统资源。如果把一个进程看做一个小的车间,车间里有很多工人,有的负责操作机器,有的负责搬运材料,每个工人可以看做一个线程,线程可以共享进程的资源。可以说,线程是进程的最小单位,一个进程可以包含多个线程。

JavaScript 在设计之初便是单线程,程序运行时,只有一个线程存在,在特定的时候只能有特定的代码被执行。这和 JavaScript 的用途有关,它是一门浏览器脚本语言,通常是用来操作 DOM 的,如果是多线程,一个线程进行了删除 DOM 操作,另一个添加 DOM,此时该如何处理?所以 JavaScript 在设计之初便是单线程的。

虽然 HTML5 增加了 Web Work可用来另开一个线程,但是该线程仍受主线程的控制,所以 JavaScript 的本质依然是单线程。

执行栈和任务队列

单线程的 JavaScript 一段一段地执行,前面的执行完了,再执行后面的,试想一个,如果前一个任务需要执行很久,比如接口请求、I/O 操作,此时后面的任务只能干巴巴地等待么?干等不仅浪费了资源,而且页面的交互程度也很差。JavaScript 意识到了这个问题,他们将任务分成了同步任务和异步任务,对于二者有不同的处理。

栈 Stack

函数调用形成了一个由若干帧组成的栈。

代码语言:javascript
复制
function foo(b) {
  let a = 10;
  return a + b + 11;
}

function bar(x) {
  let y = 3;
  return foo(x * y);
}

console.log(bar(7)); // 返回 42

当调用 bar 时,第一个帧被创建并压入栈中,帧中包含了 bar 的参数和局部变量。当 bar 调用 foo 时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含 foo 的参数和局部变量。当 foo 执行完毕然后返回时,第二个帧就被弹出栈(剩下 bar 函数的调用帧)。当 bar 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了

堆 Heap

对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。

在计算机科学中,堆(Heap)是一种常见的数据结构。它是一个特殊的完全二叉树(或者可以看作是一个数组),其中每个节点都满足堆属性。

堆通常用于实现优先队列(Priority Queue)和动态的、可高效地找到最大或最小元素的数据结构。

根据堆属性的不同,堆分为两种类型:

  1. 最大堆(Max Heap):在最大堆中,每个节点的值都大于或等于其子节点的值。这意味着堆的根节点具有最大的值。
  2. 最小堆(Min Heap):在最小堆中,每个节点的值都小于或等于其子节点的值。这意味着堆的根节点具有最小的值。

堆的主要操作包括插入和删除操作:

  • 插入操作:将一个新元素插入堆中时,需要保持堆属性。具体操作是将元素添加到堆的末尾,然后通过与父节点比较并交换位置的方式向上调整堆,直到满足堆属性。
  • 删除操作:删除堆顶元素时,也需要保持堆属性。具体操作是将堆顶元素与堆的最后一个元素交换位置,然后删除堆的最后一个元素。接着,通过与子节点比较并交换位置的方式向下调整堆,直到满足堆属性。

堆的插入和删除操作的时间复杂度都是 O(log n),其中 n 是堆中元素的数量。这使得堆非常适合用于需要频繁地插入和删除元素的场景。

值得注意的是,堆不是按照某种特定的排序顺序来排列元素的,而是确保根节点具有最大或最小的值。因此,除了找到最大或最小元素外,堆中的其他元素之间并没有特定的顺序关系。

总结起来,堆是一种用于实现优先队列和高效查找最大或最小元素的数据结构。它具有快速的插入和删除操作,并且可以根据需要实现最大堆或最小堆。

队列 Queue

一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。

在 事件循环期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。

函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。

JavaScript 运行时

JavaScript 在运行时会将变量存放在堆(heap)和栈(stack)中,堆中通常存放着一些对象,而变量及对象的指针则存放在栈中。JavaScript 在执行时,同步任务会排好队,在主线程上按照顺序执行,前面的执行完了再执行后面的,排队的地方叫执行栈(execution context stack)。JavaScript 对异步任务不会停下来等待,而是将其挂起,继续执行执行栈中的同步任务,当异步任务有返回结果时,异步任务会加入与执行栈不一样的队列,即任务队列(task queue),所以任务队列中存放的是异步任务执行完成后的结果,通常是回调函数。

当执行栈的同步任务已经执行完成,此时主线程闲下来,它便会去查看任务队列是否有任务,如果有,主线程会将最先进入任务队列的任务加入到执行栈中执行,执行栈中的任务执行完了之后,主线程便又去任务队列中查看是否有任务可执行。主线程去任务队列读取任务到执行栈中去执行,这个过程是循环往复的,这便是 Event Loop,事件循环。

网上有张流传甚广的图对这一过程进行了总结,在图中我们可以看到,JavaScript 在运行时产生了堆和栈,ajax、setTimeout 等异步任务被挂起,异步任务的返回结果加入任务队列,主线程会循环往复地读取任务队列中的任务,加入执行栈中执行。

为了更好的理解 JavaScript 的执行机制,我们来看个小例子。

代码语言:javascript
复制
console.log(1)

setTimeout(function() {

console.log(2)

}, 300)

console.log(3)

输出的结果是 1,3,2。setTimeout 是一个定时器,延迟 300 毫秒执行,所以 300 毫秒后,打印 2 的回调函数才会进入任务队列,等到执行栈中的代码执行完成后,也就是打印出 1 和 3 后,打印出 2 的回调函数才进入执行栈执行。

如果将 setTimeout 的第二个参数设置为 0,它表示主线程空闲之后尽早执行它的回调,HTML5 规定 setTimeout 的第二个参数不得小于 4 毫秒。

代码语言:javascript
复制
setTimeout(function() {

console.log(1)

}, 0)

console.log(2)


// 2,1

对于 setTimeout 还有一个需要注意的是,它的延迟时间并不是等待多少毫秒后就一定会执行,始终是要等待主线程已经空闲了才会去读取它,如果执行栈中的任务需要很长时间才能执行完,那任务队列中的任务只能等待。我们可以通过一个例子来体验一下。

代码语言:javascript
复制
var enterTime = Date.now()




function sleep(time) {

for(var temp = Date.now(); Date.now() - temp <= time;);

}




setTimeout(function() {

var exeTime = Date.now()

console.log(exeTime - enterTime)

}, 300)




sleep(1000) // 睡眠 1 秒

我们定义了一个 sleep 函数,设置了 1 秒的执行时间,所以 setTimeout 要等待的时间肯定大于 1 秒,而不是 300 毫秒后就执行了。上述代码的执行结果是 1000 左右,值不固定,可以复制代码到控制台执行看看。

宏任务与微任务

异步任务有更深一层的划分,它们是宏任务(macro task)和微任务(micro task),二者的执行顺序也有差别。在上面我们讲到异步任务的结果会进入任务队列中,对于不同的事件类型,宏任务会加入宏任务队列,微任务会加入微任务队列。

常见的宏任务有 script(整体代码),setTimeout,setInterval;常见的微任务有 new Promise、process.nextTick(node.js 环境)。

在执行栈空的时候,主线程会从任务队列中取任务来执行,其过程如下: 1.选择最先进入队列的宏任务执行(最开始是 script 整体代码) 2.检查是否存在微任务,如果存在,执行微任务队列中得所以任务,直至清空微任务队列 3.重复以上步骤

我们来通过代码体验一下宏任务与微任务的执行顺序。

代码语言:javascript
复制
console.log(1)

setTimeout(function() {

console.log(2)

new Promise(function(resolve) {

console.log(3)

resolve(4)

}).then(function(num) {

console.log(num)

})

}, 300)


new Promise(function(resolve) {

console.log(5)

resolve(6)

}).then(function(num) {

console.log(num)

})

setTimeout(function() {

console.log(7)

}, 400)

我们一步步来分析上面的执行顺序,这段代码作为宏任务进入主线程开始执行,首先打印出 1,然后遇到了 setTimeout,主程序将它挂起,300 毫秒后它的回调函数进入宏任务队列,我们记做 setTimeout1。随后遇到了 new Promise,resolve 部分是同步执行的,所以会打印出 5,then 中的回调函数进入微任务队列,我们暂时记做 promise1。最后是 setTimeout,同理在 400 毫秒后加入了宏任务队列,我们记做 setTimeout2。此时任务队列的情况如下:

宏任务

微任务

setTimeout1

promise1

setTimeout2

此时已经执行完一个宏任务(script 整体代码),接着主线程查看微任务队列,发现存在微任务,于是把 promise1 执行了,打印出 6。此时微任务队列已经空了,任务队列的情况如下:

宏任务

微任务

setTimeout1

setTimeout2

以上便是一次循环。

接着主线程又开始查看宏任务队列,将 setTimeout1 的回调函数加入任务栈开始执行,于是首先打印出 2,之后是 3,再将 then 中的回调函数加入微任务队列,我们记做 promise2。此时任务队列的情况如下:

宏任务

微任务

setTimeout2

promise2

此时执行栈也空了,于是将微任务 promise2 加入执行栈,打印出 4。此时微任务已经执行完,这便完成了第二次循环。然后再查看宏任务队列,于是执行 setTimeout2,打印出 7。所以代码中的输出顺序是 1,5,6,2,3,4,7。需要注意的是,主线程对微任务的读取是逐个读取,直到微任务队列为空。对宏任务队列的读取在一次循环中只读取一个。

小结

在本节中,我们了解了 JavaScript 的运行机制,它是单线程的。JavaScript 中的任务可分为同步任务和异步任务,同步任务总是先进入执行栈中执行,异步任务会被挂起,直到有结果返回时,异步任务会进入任务队列中等待主线程读取执行。当执行栈为空时,主线程便会循环往复地读取任务队列中的事件,进入执行栈执行,这个过程叫 Event Loop。主线程对任务队列的读取也有先后之分,首先会读取宏任务,最开始是 script 整体代码,执行完一个宏任务后,会去查找微任务,将微任务队列的事件都执行完,这个过程也是循环往复的。 所以本篇我主要讲了:

  • JavaScript 是单线程的本质;
  • 执行栈和任务队列是什么;
  • 什么是 Event Loop;
  • 宏任务和微任务的区别。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-11-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是事件循环
  • 并发模型
  • 单线程
  • 执行栈和任务队列
  • 栈 Stack
  • 堆 Heap
  • 队列 Queue
  • JavaScript 运行时
  • 宏任务与微任务
  • 小结
相关产品与服务
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档