文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

面试率超高的JS事件循环,看这篇就够了

2024-12-01 13:32

关注

大家好,我是 CUGGZ。

事件循环是 JavaScript 中一个非常重要的概念,下面就来看看浏览器和 Node.js 中的事件循环的原理,以及两者之间的差异!

1、异步执行原理

(1)单线程的JavaScript

我们知道,JavaScript是一种单线程语言,它主要用来与用户互动,以及操作DOM。

JavaScript 有同步和异步的概念,这就解决了代码阻塞的问题:

那单线程有什么好处呢?

(2)多线程的浏览器

JS 是单线程的,在同一个时间只能做一件事情,那为什么浏览器可以同时执行异步任务呢?

这是因为浏览器是多线程的,当 JS 需要执行异步任务时,浏览器会另外启动一个线程去执行该任务。也就是说,JavaScript是单线程的指的是执行JavaScript代码的线程只有一个,是浏览器提供的JavaScript引擎线程(主线程)。除此之外,浏览器中还有定时器线程、 HTTP 请求线程等线程,这些线程主要不是来执行 JS 代码的。

比如主线程中需要发送数据请求,就会把这个任务交给异步 HTTP 请求线程去执行,等请求数据返回之后,再将 callback 里需要执行的 JS 回调交给 JS 引擎线程去执行。也就是说,浏览器才是真正执行发送请求这个任务的角色,而 JS 只是负责执行最后的回调处理。所以这里的异步不是 JS 自身实现的,而是浏览器为其提供的能力。

下图是Chrome浏览器的架构图:

可以看到,Chrome不仅拥有多个进程,还有多个线程。以渲染进程为例,就包含GUI渲染线程、JS引擎线程、事件触发线程、定时器触发线程、异步HTTP请求线程。这些线程为 JS 在浏览器中完成异步任务提供了基础。

2、浏览器事件循环

JavaScript的任务分为两种同步和异步:

上面提到了任务队列和执行栈,下面就先来看看这两个概念。

(1)执行栈与任务队列

执行栈:从名字可以看出,执行栈使用到的是数据结构中的栈结构, 它是一个存储函数调用的栈结构,遵循先进后出的原则。它主要负责跟踪所有要执行的代码。 每当一个函数执行完成时,就会从堆栈中弹出(pop)该执行完成函数;如果有代码需要进去执行的话,就进行 push 操作。以下图为例:

当执行这段代码时,首先会执行一个 main 函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈,在图中也可以发现,foo 函数后执行,当执行完毕后就从栈中弹出了。

JavaScript在按顺序执行执行栈中的方法时,每次执行一个方法,都会为它生成独有的执行环境(上下文),当这个方法执行完成后,就会销毁当前的执行环境,并从栈中弹出此方法,然后继续执行下一个方法。

任务队列: 从名字中可以看出,任务队列使用到的是数据结构中的队列结构,它用来保存异步任务,遵循先进先出的原则。它主要负责将新的任务发送到队列中进行处理

JavaScript在执行代码时,会将同步的代码按照顺序排在执行栈中,然后依次执行里面的函数。当遇到异步任务时,就将其放入任务队列中,等待当前执行栈所有同步代码执行完成之后,就会从异步任务队列中取出已完成的异步任务的回调并将其放入执行栈中继续执行,如此循环往复,直到执行完所有任务。

JavaScript任务的执行顺序如下:

在事件驱动的模式下,至少包含了一个执行循环来检测任务队列中是否有新任务。通过不断循环,去取出异步任务的回调来执行,这个过程就是事件循环,每一次循环就是一个事件周期。

(2)宏任务和微任务

任务队列其实不止一种,根据任务种类的不同,可以分为微任务(micro task)队列宏任务(macro task)队列。常见的任务如下:

任务队列执行顺序如下:

可以看到,Eventloop 在处理宏任务和微任务的逻辑时的执行情况如下:

  1. JavaScript 引擎首先从宏任务队列中取出第一个任务;
  2. 执行完毕后,再将微任务中的所有任务取出,按照顺序分别全部执行(这里包括不仅指开始执行时队列里的微任务),如果在这一步过程中产生新的微任务,也需要执行,也就是说在执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行。
  3. 然后再从宏任务队列中取下一个,执行完毕后,再次将 microtask queue 中的全部取出,循环往复,直到两个 queue 中的任务都取完。

也是就是说,一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务。

下面通过一个例子来体会事件循环:

console.log('同步代码1');

setTimeout(() {
console.log('setTimeout')
}, 0)

new Promise((resolve) => {
console.log('同步代码2')
resolve()
}).then(() {
console.log('promise.then')
})

console.log('同步代码3');

代码输出结果如下:

"同步代码1"
"同步代码2"
"同步代码3"
"promise.then"
"setTimeout"

那这段代码执行过程是怎么的呢?

  1. 遇到第一个console,它是同步代码,加入执行栈,执行并出栈,打印出"同步代码1"。
  2. 遇到setTimeout,它是一个宏任务,加入宏任务队列。
  3. 遇到new Promise 中的console,它是同步代码,加入执行栈,执行并出栈,打印出"同步代码2"。
  4. 遇到Promise then,它是一个微任务,加入微任务队列。
  5. 遇到第三个console,它是同步代码,加入执行栈,执行并出栈,打印出"同步代码3"。
  6. 此时执行栈为空,去执行微任务队列中所有任务,打印出"promise.then"。
  7. 执行完微任务队列中的任务,就去执行宏任务队列中的一个任务,打印出"setTimeout"。

从上面的宏任务和微任务的工作流程中,可以得出以下结论:

那么问题来了,为什么要将任务队列分为微任务和宏任务呢,他们之间的本质区别是什么呢?

JavaScript在遇到异步任务时,会将此任务交给其他线程来执行(比如遇到setTimeout任务,会交给定时器触发线程去执行,待计时结束,就会将定时器回调任务放入任务队列等待主线程来取出执行),主线程会继续执行后面的同步任务。

对于微任务,比如promise.then,当执行promise.then时,浏览器引擎不会将异步任务交给其他浏览器的线程去执行,而是将任务回调存在一个队列中,当执行栈中的任务执行完之后,就去执行promise.then所在的微任务队列。

所以,宏任务和微任务的本质区别如下:

3、Node.js的事件循环

(1)事件循环的概念

对于Node.js的事件循环,官网的描述如下:

When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.

翻译一下就是:当Node.js启动时,它会初始化一个事件循环,来处理输入的脚本,这个脚本可能进行异步API的调用、调度计时器或调用process.nextTick(),然后开始处理事件循环。

JavaScript和Node.js是基于V8 引擎的,浏览器中包含的异步方式在 NodeJS 中也是一样的。除此之外,Node.js中还有一些其他的异步形式:

这些异步任务的执行就需要依靠Node.js的事件循环机制了。

Node.js 中的 Event Loop 和浏览器中的是完全不相同的东西。Node.js使用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现的,如下图所示:

根据上图,可以看到Node.js的运行机制如下:

  1. V8引擎负责解析JavaScript脚本。
  2. 解析后的代码,调用Node API。
  3. libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
  4. V8引擎将结果返回给用户。

(2)事件循环的流程

其中libuv引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。下面 是Eventloop 事件循环的流程:

整个流程分为六个阶段,当这六个阶段执行完一次之后,才可以算得上执行了一次 Eventloop 的循环过程。下面来看下这六个阶段都做了哪些事:

  1. timers 阶段:执行timer(setTimeout、setInterval)的回调,由 poll 阶段控制。
  2. I/O callbacks 阶段:主要执行系统级别的回调函数,比如 TCP 连接失败的回调。
  3. idle, prepare 阶段:仅Node.js内部使用,可以忽略。
  4. poll 阶段:轮询等待新的链接和请求等事件,执行 I/O 回调等。
  5. check 阶段:执行 setImmediate() 的回调。
  6. close callbacks 阶段:执行关闭请求的回调函数,比如socket.on('close', ...)。

注意:上面每个阶段都会去执行完当前阶段的任务队列,然后继续执行当前阶段的微任务队列,只有当前阶段所有微任务都执行完了,才会进入下个阶段,这里也是与浏览器中逻辑差异较大的地方。

其中,这里面比较重要的就是第四阶段:poll,这一阶段中,系统主要做两件事:

在进入该阶段时如果没有设定了 timer 的话,会出现以下情况:

(1)如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制。

(2)如果 poll 队列为空时,会出现以下情况:

当设定了 timer 且 poll 队列为空,则会判断是否有 timer 超时,如果有的就会回到 timer 阶段执行回调。

这一过程的具体执行流程如下图所示:

(3)宏任务和微任务

Node.js事件循环的异步队列也分为两种:宏任务队列和微任务队列。

(4)process.nextTick()

上面提到了process.nextTick(),它是node中新引入的一个任务队列,它会在上述各个阶段结束时,在进入下一个阶段之前立即执行。

Node.js官方文档的解释如下:

process.nextTick()is not technically part of the event loop. Instead, thenextTickQueuewill be processed after the current operation is completed, regardless of the current phase of the event loop. Here, an operation is defined as a transition from the underlying C/C++ handler, and handling the JavaScript that needs to be executed.

例如下面的代码:

setTimeout(() {
console.log('timeout');
}, 0);

Promise.resolve().then(() {
console.error('promise')
})

process.nextTick(() {
console.error('nextTick')
})

输出结果如下:

nextTick
promise
timeout

可以看到,process.nextTick()是优先于promise的回调执行。

(5)setImmediate 和 setTimeout

上面还提到了setImmediate 和 setTimeout,这两者很相似,主要区别在于调用时机的不同:

例如下面的代码:

setTimeout(() {
console.log('timeout');
}, 0);
setImmediate(() {
console.log('setImmediate');
});

输出结果如下:

timeout
setImmediate

在上面代码的执行过程中,第一轮循环后,分别将 setTimeout  和 setImmediate 加入了各自阶段的任务队列。第二轮循环首先进入timers 阶段,执行定时器队列回调,然后 pending callbacks和poll 阶段没有任务,因此进入check 阶段执行 setImmediate 回调。所以最后输出为timeout、setImmediate。###= 4. Node与浏览器事件循环的差异 Node.js与浏览器的事件循环的差异如下:

Nodejs和浏览器的事件循环流程对比如下:

  1. 执行全局的 Script 代码(与浏览器无差)。
  2. 把微任务队列清空:注意,Node 清空微任务队列的手法比较特别。在浏览器中,我们只有一个微任务队列需要接受处理;但在 Node 中,有两类微任务队列:next-tick 队列和其它队列。其中这个 next-tick 队列,专门用来收敛 process.nextTick 派发的异步任务。在清空队列时,优先清空 next-tick 队列中的任务,随后才会清空其它微任务
  3. 开始执行 macro-task(宏任务)。注意,Node 执行宏任务的方式与浏览器不同:在浏览器中,我们每次出队并执行一个宏任务;而在 Node 中,我们每次会尝试清空当前阶段对应宏任务队列里的所有任务(除非达到系统限制)。
  4. 步骤3开始,会进入 3 -> 2 -> 3 -> 2…的循环。
来源:前端充电宝内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯