事件循环机制解密:解锁JavaScript异步编程的高效之路

事件循环机制解密:解锁JavaScript异步编程的高效之路

技术博客 admin 502 浏览

1. 引入和背景知识

  • 简短介绍:JavaScript是一种单线程编程语言,这意味着在任何给定时刻,它只能执行一个任务。然而,这并不意味着JavaScript不能处理并发操作,比如网络请求、DOM操作或者计时器触发。这一切都归功于其独特的事件循环机制。事件循环允许JavaScript在等待某些操作完成的同时继续执行其他任务,从而实现非阻塞式编程。

简单点来说:事件循环机制弥补了单线程语言js不能处理并发操作的缺点,使其不需要等待某些操作的完成就去执行其他任务,大大节省了时间和资源。

  • 历史背景:事件循环的概念最早出现在早期的GUI系统中,但真正使其流行的是Node.js。Node.js是一个基于Chrome V8引擎的JavaScript运行环境,它利用事件驱动和非阻塞I/O模型,能够轻松地构建高性能、可扩展的网络应用。事件循环在Node.js中的应用,极大地提高了JavaScript处理高并发的能力,同时也影响了浏览器端JavaScript的异步处理方式。

在理解事件循环之前,你需要理解同步和异步的概念

同步:代码从上往下执行,遇到耗时的代码需等待完成才能执行下一行代码。

同步代码示例

Javascript
复制代码
function syncFunction() { console.log('Start'); const result = someTimeConsumingOperation(); // 假设这是一个耗时的操作 console.log('Result:', result); console.log('End'); } syncFunction();

在上面的例子中,someTimeConsumingOperation会阻塞后续代码的执行,等它完成,后面的代码才能执行。

异步:遇到需要耗时的代码,那就先挂起,先执行不耗时的代码,等到不耗时的代码执行完了,执行引擎腾出手了,再来执行耗时代码。

异步代码示例

Javascript
复制代码
function asyncFunction() { console.log('Start'); someAsyncOperation((result) => { // 异步操作,传入回调函数 console.log('Result:', result); }); console.log('End'); } asyncFunction();

在这个例子中,someAsyncOperation接受一个回调函数作为参数。它立即返回,允许后面的console.log('End');立即执行。当someAsyncOperation完成后,它会调用回调函数,打印结果。所以打印顺序为StartEnd,最后才是result变量。

2.事件循环机制定义和作用

  • 定义:事件循环是 JavaScript 中一种重要的异步执行机制
  • 作用:管理和协调各种异步任务的执行顺序,保证 JavaScript 代码的执行顺序预期一致。(简单点理解:我们自己写的代码我们是能够理解它的一个执行顺序的,这是你的预期,而执行引擎需要靠着事件循环机制来保证真实的执行顺序不会偏离你预期。)

那么事件循环机制又是如何发挥它的作用呢?这是关键。

3.事件循环组成部分

  • 主线程(调用栈):执行任务;
  • 任务队列:存放异步任务;
  • 事件循环线程:检查任务队列里面的异步任务,有的话将异步任务调入主线程执行。

异步任务分为宏任务微任务,因此队列也分为宏任务队列微任务队列

宏任务和微任务划分

微任务promise.then(), process.nextTick(), MutationObserver

宏任务: script, setTimeout, setInterval, I/O, UI-readering

4.事件循环机制的执行

  1. 执行同步代码(初始的同步任务可以看作是一次宏任务的执行)
  2. 同步执行完毕后,检查队列中是否有异步需要执行
  3. 执行所有的异步微任务
  4. 异步微任务执行完毕后,如果有需要就会渲染页面
  5. 执行异步宏任务,即开启下一次事件循环(之后循环执行2,3,4,5,直到执行完全部代码结束)

根据上面的执行步骤,我们可以画出执行流程图:

执行步骤和流程图看起来会有点抽象,下面我会举出实例结合步骤和流程图来说明和训练,帮助你去掌握它。

5.实例训练

例1:

js
复制代码
console.log(1); new Promise((resolve, reject) => { console.log(2); resolve() }) .then(() => { console.log(3); setTimeout(() => { console.log(4); }, 0) }) setTimeout(() => { console.log(5); setTimeout(() => { console.log(6); }, 0) }, 0) console.log(7);

面试官写出如上代码,请问输出顺序结果?为什么?

为了易于讲解,将setTimeout出现的顺序,从上往下编号为set1set2set3,如下:

代码console.log(4);所在的setTimeoutset1

代码console.log(5);所在的setTimeoutset2

代码console.log(6);所在的setTimeoutset3

这是执行步骤:

  1. 执行同步代码(初始的同步任务可以看作是一次宏任务的执行)
  2. 同步执行完毕后,检查队列中是否有异步需要执行
  3. 执行所有的异步微任务
  4. 异步微任务执行完毕后,如果有需要就会渲染页面
  5. 执行异步宏任务,即开启下一次事件循环(之后循环执行2,3,4,5,直到执行完全部代码结束)

我们现在按照步骤走一遍:

  • 第一步:执行同步代码:

console.log(1);new Promise...console.log(7);为同步代码执行,输出1,2,7

  • 第二步:检查队列中是否有异步需要执行:

.then()为异步微任务,入微任务队列,如下:

set2为异步宏任务,入宏任务队列,如下:

  • 第三步:执行所有的异步微任务

.then出队列执行,console.log(3);执行输出3,而set1属于微任务里的宏任务,不执行,入宏任务队列,如下:

  • 第四步:异步微任务执行完毕后,如果有需要就会渲染页面

这个例子不需要渲染页面,所以可跳过。

  • 第五步:执行异步宏任务,即开启下一次事件循环。

set2出队列执行,开启下一次事件循环,console.log(5);同步代码执行输出5,set3为异步宏任务,入队列,如下:

  • 第二次循环第二步:检查队列中是否有异步需要执行:

有,set1为异步宏任务。

  • 第二次循环第三步:执行所有的异步微任务

无,跳过。

  • 第二次循环第四步:异步微任务执行完毕后,如果有需要就会渲染页面

无,跳过。

  • 第二次循环第五步:执行异步宏任务,即开启下一次事件循环

set1为异步宏任务,出队列,执行输出4,如下:

开启第三次事件循环...

  • ...

  • ...

  • ...

  • 最后set3出队列,console.log(6);输出6,如下:

开启第四次事件循环...

  • ...

结束

输出:1 2 7 3 5 4 6

例2

由于本文篇幅过长,下面的例子就当课后题。

js
复制代码
console.log('script start'); async function async1() { await async2() console.log('async1 end'); } async function async2() { console.log('async2 end'); } async1() setTimeout(function() { console.log('setTimeout'); }, 0) new Promise(function(resolve, reject) { console.log('promise'); resolve() }) .then(() => { console.log('then1'); }) .then(() => { console.log('then2'); }) console.log('script end');

结果:

答错的掘友,遗漏了await的影响。

提示:await 会将后序的代码阻塞进微任务队列。

6.总结与建议

总结

事件循环是JavaScript处理异步操作的核心机制,它通过维护任务队列(包括宏任务和微任务队列),在执行栈清空时调度任务执行。事件循环保证了JavaScript的单线程特性不会成为性能瓶颈,同时使得异步操作能够无缝集成到程序中。

优化建议

  1. 合理安排异步操作: 避免在循环中频繁触发异步操作,这可能导致任务队列过载,影响用户体验。尽量将相关操作组合成一次异步请求。
  2. 使用Promise和async/await: 利用Promise和async/await简化异步代码,提高代码的可读性和维护性。它们不仅使代码更加简洁,而且能更准确地控制异步操作的执行顺序。
  3. 避免长时间运行的同步操作: 将可能消耗大量计算资源的同步操作转换为异步操作,或者将它们拆分为多个小任务,以减少阻塞主线程的时间。
  4. 合理使用setTimeout和setInterval: 在使用定时器时,考虑到事件循环的机制,避免设置过短的延迟时间,以免造成不必要的CPU负担。
  5. 监控和优化性能: 使用浏览器开发者工具或Node.js的性能分析工具定期检查应用程序的性能,找出潜在的瓶颈,并进行优化。

源文:事件循环机制解密:解锁JavaScript异步编程的高效之路

如有侵权请联系站点删除!

Technical cooperation service hotline, welcome to inquire!