彻底搞懂JavaScript事件循环(上)

简介: 1. 异步执行原理(1)单线程的JavaScript我们知道,JavaScript是一种单线程语言,它主要用来与用户互动,以及操作DOM。JavaScript 有同步和异步的概念,这就解决了代码阻塞的问题:

1. 异步执行原理


(1)单线程的JavaScript

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

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

  • 同步:如果在一个函数返回的时候,调用者就能够得到预期结果,那么这个函数就是同步的;
  • 异步:如果在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。

那单线程有什么好处呢?

  • 在 JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。这是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。
  • 得益于 JS 是单线程运行的,可以达到节省内存,节约上下文切换时间的好处。


(2)多线程的浏览器

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

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

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

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


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


2. 浏览器的事件循环


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

  • 同步任务: 在主线程上排队执行的任务,只有一个任务执行完毕,才能执行下一个任务,
  • 异步任务: 不进入主线程,而是放在任务队列中,若有多个异步任务则需要在任务队列中排队等待,任务队列类似于缓冲区,任务下一步会被移到执行栈然后主线程执行调用栈的任务。

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


(1)执行栈与任务队列

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


网络异常,图片无法展示
|

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


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


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


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

JavaScript任务的执行顺序如下:


网络异常,图片无法展示
|


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


(2)宏任务和微任务


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

  • 宏任务: script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
  • 微任务: Promise、MutaionObserver、process.nextTick(Node.js 环境);

任务队列执行顺序如下:


网络异常,图片无法展示
|


可以看到,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"

注:更多异步代码执行示例,详见文章:  《「2021」高频前端面试题汇总之代码输出结果篇》


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

  • 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
  • 微任务的执行时长会影响当前宏任务的时长。比如一个宏任务在执行过程中,产生了 10 个微任务,执行每个微任务的时间是 10ms,那么执行这 10 个微任务的时间就是 100ms,也可以说这 10 个微任务让宏任务的执行时间延长了 100ms。
  • 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行(优先级更高)。


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


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中还有一些其他的异步形式:

  • 文件 I/O:异步加载本地文件。
  • setImmediate():与 setTimeout 设置 0ms 类似,在某些同步任务完成后立马执行。
  • process.nextTick():在某些同步任务完成后立马执行。
  • server.close、socket.on('close',...)等:关闭回调。

这些异步任务的执行就需要依靠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引擎将结果返回给用户;



相关文章
|
3月前
|
设计模式 JavaScript API
Node.js 事件循环
Node.js 事件循环
26 2
|
4月前
|
前端开发 JavaScript UED
深入理解JavaScript中的事件循环机制
JavaScript中的事件循环机制是其异步编程的核心,深入理解该机制对于开发高效、流畅的前端应用至关重要。本文将介绍事件循环的工作原理、常见的事件循环模型,以及如何利用这些知识解决前端开发中的常见问题。
|
6天前
|
JavaScript API 数据库
深入理解Node.js事件循环及其在后端开发中的应用
【9月更文挑战第3天】本文将深入浅出地介绍Node.js的事件循环机制,探讨其非阻塞I/O模型和如何在后端开发中利用这一特性来处理高并发请求。通过实际的代码示例,我们将看到如何有效地使用异步操作来优化应用性能。文章旨在为读者揭示Node.js在后端开发中的核心优势和应用场景,帮助开发者更好地理解和运用事件循环来构建高性能的后端服务。
|
14天前
|
机器学习/深度学习 人工智能 自然语言处理
深度学习中的图像识别技术深入理解Node.js事件循环及其在后端开发中的应用
【8月更文挑战第27天】本文将介绍深度学习中的图像识别技术,包括其原理、应用领域及未来发展。我们将探讨如何通过神经网络实现图像识别,并分析其在医疗、交通等领域的应用。最后,我们将展望图像识别技术的发展前景。
|
14天前
|
运维 Cloud Native JavaScript
云端新纪元:云原生技术深度解析深入理解Node.js事件循环及其在异步编程中的应用
【8月更文挑战第27天】随着云计算技术的飞速发展,云原生已成为推动现代软件开发和运维的关键力量。本文将深入探讨云原生的基本概念、核心价值及其在实际业务中的应用,帮助读者理解云原生如何重塑IT架构,提升企业的创新能力和市场竞争力。通过具体案例分析,我们将揭示云原生技术背后的哲学思想,以及它如何影响企业决策和操作模式。
|
14天前
|
运维 JavaScript 安全
自动化运维:使用Ansible简化日常任务深入理解Node.js事件循环和异步编程
【8月更文挑战第27天】在快节奏的技术环境中,自动化不再是奢侈品,而是必需品。本文将引导你通过Ansible实现自动化运维,从基础到高级应用,解锁高效管理服务器群的秘诀,让你的IT操作更加流畅和高效。
|
4月前
|
Web App开发 JavaScript 前端开发
浏览器与Node.js事件循环:异同点及工作原理
浏览器与Node.js事件循环:异同点及工作原理
|
10天前
|
JavaScript 前端开发 开发者
深入理解Node.js中的事件循环
【8月更文挑战第31天】在Node.js的世界里,“事件循环”是心脏,它驱动着异步操作的脉搏。本文将带你走进事件循环的核心,通过简单的例子揭示其神秘面纱。你将看到如何利用事件循环来处理并发任务,以及它是如何在背后默默支撑起你的后端应用。准备好,让我们一起探索这个让Node.js与众不同的特性吧!
|
10天前
|
JavaScript 开发者
深入理解Node.js事件循环及其在后端开发中的应用
【8月更文挑战第31天】 本文将带你走进Node.js的事件循环机制,通过浅显易懂的语言和实例代码,揭示其背后的工作原理。我们将一起探索如何高效利用事件循环进行异步编程,提升后端应用的性能和响应速度。无论你是Node.js新手还是有一定经验的开发者,这篇文章都能给你带来新的启发和思考。
|
10天前
|
JavaScript 前端开发 数据库
深入理解Node.js中的事件循环和异步编程
【8月更文挑战第31天】 本文旨在揭示Node.js中事件循环的神秘面纱,通过浅显易懂的语言和生动的比喻,我们将一同走进异步编程的世界。你将了解事件循环如何协调任务执行,掌握异步操作背后的原理,并通过实际代码示例,学会如何在Node.js中高效地处理异步任务。让我们开始探索这段奇妙的旅程!
下一篇
DDNS