一道面试题,解锁JavaScript中的Event-Loop事件循环机制(面试必考)

面试题

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(resolve => {
    console.log('Promise')
    resolve()
})
    .then(function () {
        console.log('promise1')
    })
    .then(function () {
        console.log('promise2')
    })
console.log('script end')

大家看看上面这道题!能告诉我输出结果是什么吗?

如果你还不能确定,那么学完今天的内容,你就知道这道题该怎么做了!我们会在今天的结尾为大家揭示答案,及分析过程!

那么,接下来就开始我们今天的学习吧!

一、进程和线程

1、什么是进程?

在计算机科学中,进程是指在系统中运行的程序的实例。它是计算机中的一个独立执行单元,包含了程序代码、数据以及程序的执行状态。每个进程都有其独立的内存空间,不同进程之间一般是相互隔离的,这样可以确保它们不会直接干扰彼此的执行。

就好比你打开电脑的任务管理器,你看到一个又一个的程序就是你的电脑的进程!也可以理解为CPU运行指令和保存上下文所需的时间!

2、什么是线程?

进程中的更小的单位,描述了一段指令执行所需的时间,在计算机科学中,线程是指进程内的一个执行单元,它包含了执行程序的代码和相关的上下文信息。线程是进程的一部分,一个进程可以包含多个线程,这些线程共享相同的资源,如内存空间和文件句柄。线程之间的切换相对于进程切换来说更加轻量级,因为它们共享相同的地址空间。

有一点我们要记住一点JS引擎线程和浏览器的渲染进程是互斥的,会造成不安全的渲染!

面试题:打开一个tap页面,输入一个url回车到页面展现的过程。问:这中间发生了什么?

我们新开的一个页面,就是新开一个进程,需要多个线程配合才能完成页面的展示,其中有很多的细节,

大致可以分为三步:1、渲染线程(GPU) ,2、http请求进程,3、JS引擎线程

**什么是GPU?**GUP它是属于浏览器的,具有绘制功能,将页面上的展示绘制出来!就比如找到哪个物理发光点 应该亮什么灯,我们可以把它理解成一个画笔,作用就是把整个页面画出来。

3、JS是单线程

JavaScript语言是一门单线程语言,这意味着,在同一时间只能执行一个任务。

正是因为JS单线程的这种特点,给它带来一些优点!

  1. 简单性: 单线程模型使得编写和调试JavaScript代码相对简单。开发人员不必担心多线程的同步和竞态条件问题,这降低了代码的复杂性。
  2. 避免竞态条件: 多线程环境下,如果没有正确处理同步,可能会导致竞态条件(Race Condition)。单线程避免了这种情况,因为在任何给定时刻只有一个任务在执行。
  3. 前端开发中的简便性: 在Web开发中,JavaScript主要用于操作DOM、处理用户交互等,这些任务通常不需要多线程。单线程的模型对于处理这些任务是足够的,并且简化了前端开发的复杂性。
  4. 更好的可控性: 单线程模型使得代码的执行流程更加可控。事件循环(Event Loop)机制确保任务按照特定的顺序执行,使得开发者更容易理解代码的执行流程。
  5. 资源节省: 单线程模型节省了在多线程切换上的一些开销。在多线程环境中,线程的切换可能会引入额外的开销,而单线程模型避免了这个问题。
  6. 避免死锁: 多线程应用程序中,死锁是一个潜在的问题,可能会导致程序无响应。单线程模型避免了这个问题,因为只有一个执行线程。

但是,在某些情况下,也可能导致性能问题,特别是在处理大量计算或需要等待的异步任务时。为了克服这些限制,JavaScript使用异步编程模型,允许开发者利用单线程的同时,通过异步操作提高并发性。

有人就要问了,为什么JS不设计成多线程的?

这是因为JS设计之初是打算设计成浏览器的脚本语言,多线程 高并发高功耗,多线程运行的时候会多占一些内存,是为了遏制同时干多个事情的能力,节约进程内存的开销。

接下来,我们就为大家介绍JavaScript中异步 编程!(我们在之前的文章,为大家初介绍过异步和它的Promise处理机制:[ES6]新方法--Promise 手搓异步,拳打回调 ,拿捏同步 - 掘金 (juejin.cn))

二、JavaScript中的异步

JavaScript异步编程中存在两个概念!宏任务和微任务!

1、宏任务(异步很凶的代码)

  1. 整体的代码(Script): 整体的 JavaScript 代码作为一个宏任务执行。
  2. 定时器事件(setTimeout、setInterval): 设置定时器的回调函数会作为一个宏任务执行。
  3. 事件监听器(Event Listeners): 通过事件监听器绑定的回调函数会作为宏任务执行。
  4. I/O 操作: 执行某些 I/O 操作时,例如文件读写、网络请求,会作为宏任务执行。
  5. 用户交互事件: 用户的交互事件,例如点击、键盘输入,会触发相应的回调函数,作为宏任务执行。
  6. UI-rendering: 页面渲染 重要

2、微任务

  1. Promise 的回调函数: Promise 的 thencatch 方法中的回调函数会作为微任务执行。
  2. MutationObserver: 使用 MutationObserver 监听 DOM 变化的回调函数会作为微任务执行。
  3. process.nextTick(Node.js): 在 Node.js 中,process.nextTick 中的回调函数会作为微任务执行。
  4. queueMicrotask: queueMicrotask 函数添加的回调函数会作为微任务执行。
  5. Object.observe(已废弃): Object.observe 中的回调函数会作为微任务执行(注意:Object.observe 已被废弃)。

我们了解这两个概念有什么用呢?

接下来就介绍我们今天的应用!

三、事件循环机制event-loop 面试必考

事件循环机制的步骤如下:

  1. 执行同步代码(这属于宏任务)
  2. 当执行栈为空,去查询是否有异步代码需要执行
  3. 执行微任务
  4. 如果有需要,会渲染页面d
  5. 执行宏任务,(这也叫下一轮的event-loop的开启)

接下来我们看看这个案例:

js 复制代码
let a = 1
console.log(a);
// v8决定定时器耗时
setTimeout(()=>{
    console.log(a);
},1000)
let b = 2
//for循环是由我们CPU说要耗时的,v8眼里它是不耗时的
for(let i = 0;i<10000;i++)
{
    console.log(b);
}

在这个案例当中,在浏览器引擎眼里,for循环是不会耗时的,但是我们的CPU运行需要耗时,我们假设for执行完需要1s,那么,我们的setTimeout需要耗时多少?答案是2s!for执行完的时间由我们的CPU决定,它是一个同步代码。

你也许会疑惑,这里也没有用到事件循环机制阿?对!那我们用事件循环机制来分析下面这个案例

js 复制代码
console.log('starting');
setTimeout(()=>{
    console.log('settimeout');
    setTimeout(()=>{
        console.log('inner');
    })
    console.log('end');
},1000)
new Promise((resolve,reject)=>{
    console.log('promise');
    resolve()
})
.then(()=>{
    console.log('then');
})
.then(()=>{
    console.log('then2');
})
.then(()=>{
    console.log('then3');
})

这个案例的执行结果是什么?

我们后台数据存在两个队列!

我们开始分析这个案例:

浏览器从上往下执行,根据事件循环机制的步骤:

第一轮事件循环机制

一、执行同步代码

​ 1、第一行console.log('starting');这是一个同步代码,直接执行!

​ 2、往下是第一个定时器,我们把它推入到宏任务队列:宏任务队列:setTimeout

​ 3、然后来到new Promise这个语句是一个同步代码,我们执行它的内部:console.log('promise');同步代码,输出promise,执行 resolve()

​ 4、第一个.then,这个是微任务,推入到微任务队列当中(我们用数字[1]表示先后,区别相同的标签):微任务队列:.then()[1]

​ 5、第二个.then,这个是微任务,推入到微任务队列当中:微任务队列:.then()[2],.then()[1]

​ 6、第三个.then,这个是微任务,推入到微任务队列当中:微任务队列:.then()[3],.then()[2],.then()[1]

二、当执行栈为空,去查询是否有异步代码需要执行(意思就是如果没有同步代码需要执行了,就接着往下走)

三、执行微任务

​ 1、获取微任务队列微任务队列:.then()[3],.then()[2],.then()[1],我们知道队列是先进先出,所以我们从头开始执行!

​ 2、执行.then()[1],输出then此时微任务队列:微任务队列:.then()[3],.then()[2]

​ 3、执行.then()[2],输出then2此时微任务队列:微任务队列:.then()[3]

​ 4、执行.then()[3],输出then3此时微任务队列:微任务队列:

四、如果有需要,会渲染页面d

五、执行宏任务,(这也叫下一轮的event-loop的开启)

​ 1、获取宏任务队列:宏任务队列:setTimeout

​ 2、新的一轮事件循环:宏任务队列:

第二轮事件循环

一、执行同步代码

​ 1、console.log('settimeout');同步代码直接输出settimeout

​ 2、setTimeout,推入宏任务队列:宏任务队列:setTimeout

​ 3、console.log('end');同步代码直接输出end

二、当执行栈为空,去查询是否有异步代码需要执行(意思就是如果没有同步代码需要执行了,就接着往下走)

三、执行微任务(没有微任务)

四、如果有需要,会渲染页面d

五、执行宏任务,(这也叫下一轮的event-loop的开启)

​ 1、获取宏任务队列:宏任务队列:setTimeout

​ 2、新的一轮事件循环:宏任务队列:

第三轮事件循环

一、执行同步代码

​ 1、console.log('inner');同步代码,直接输出:inner

由于没有代码了,后续内容结束!

总结下来,我们的输出应该是:

js 复制代码
输出:
starting
promise
then
then2
then3
settimeout
end
inner

没错!这就是我们想要的答案!

值得注意的是!

我们看看这个案例:

js 复制代码
function a(){
    setTimeout(()=>{
        console.log('a');
    },1000)
}
function b(){
    setTimeout(()=>{
        console.log('b');
    },500)
}
a()
b()
//输出:
//b 
//a

为什么这里是先输出b再输出a呢?

这是因为setTimeout定时器比较特殊,在宏队列中,几乎是同步执行,时间短的先输出,像上述案例,执行完成总共花费的时间为1s

好了学到这里,你也就基本懂了事件循环机制,接下来为解决我们今天的"大人物"面试题而准备吧!

async函数

我们参考MDN官方文档介绍!async 函数 - JavaScript | MDN (mozilla.org)

async 函数是使用async关键字声明的函数。async 函数是 AsyncFunction 构造函数的实例,并且其中允许使用 await 关键字。asyncawait 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 promise

返回值

一个 Promise,这个 promise 要么会通过一个由 async 函数返回的值被解决,要么会通过一个从 async 函数中抛出的(或其中没有被捕获到的)异常被拒绝。

描述

async 函数可能包含 0 个或者多个 await 表达式。await 表达式会暂停整个 async 函数的执行进程并出让其控制权,只有当其等待的基于 promise 的异步操作被兑现或被拒绝之后才会恢复进程。promise 的解决值会被当作该 await 表达式的返回值。使用 async/await 关键字就可以在异步代码中使用普通的 try/catch 代码块。

备注: await关键字只在 async 函数内有效。如果你在 async 函数体之外使用它,就会抛出语法错误 SyntaxError

备注: async/await的目的为了简化使用基于 promise 的 API 时所需的语法。async/await 的行为就好像搭配使用了生成器和 promise。

async 函数一定会返回一个 promise 对象。如果一个 async 函数的返回值看起来不是 promise,那么它将会被隐式地包装在一个 promise 中。

面试题答案

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(resolve => {
    console.log('Promise')
    resolve()
})
    .then(function () {
        console.log('promise1')
    })
    .then(function () {
        console.log('promise2')
    })
console.log('script end')

我们拿到面试题,然后进行分析。

根据我们的事件循环机制进行分析!

第一轮事件循环

一、执行同步代码

​ 1、console.log('script start')同步代码,直接输出 script start

​ 2、async函数async1async2的声明,不用管

​ 3、async1()函数调用,进入函数内部,碰到await async2(),原本我们会把它和它之后当作微任务放入微任务队列当中,现在它只会把它后面的代码放入微任务当中,而它接着的代码,被强行提前 !所以这里会先调用async2,而console.log('async1 end')进入微任务队列:微任务队列:console.log('async1 end'),而async2()会立即调用

​ 4、紧接着第三步,进入async2函数,里面没有awiatconsole.log('async2 end')为同步代码,输出 async2 end

​ 5、跳出函数调用,往下执行,一个定时器的声明。加入宏任务:宏任务队列:setTimeout[1]

​ 6、new promise为同步代码,执行里面的逻辑,console.log('Promise')输出 Promise,再调用resolve()

​ 7、.then,推入微任务队列:微任务队列:.then[1],console.log('async1 end')

​ 8、又一个.then,推入微任务队列:微任务队列:.then[2],.then[1],console.log('async1 end')

​ 9、console.log('script end')同步代码,输出 script end

二、当执行栈为空,去查询是否有异步代码需要执行(意思就是如果没有同步代码需要执行了,就接着往下走)

三、执行微任务

​ 1、获取微任务队列:微任务队列:.then[2],.then[1],console.log('async1 end')

​ 2、开始执行,输出 async1 end然后微任务队列:.then[2],.then[1]

​ 3、再执行.then[1],内部有一个输出代码console.log('promise1')输出 promise1此时微任务队列:.then[2]

​ 4、执行.then[2],内部有一个输出console.log('promise2'),输出 promise2,此时微任务队列:

四、如果有需要,会渲染页面d

五、执行宏任务,(这也叫下一轮的event-loop的开启)

​ 1、获取宏任务队列:宏任务队列:setTimeout

​ 2、新的一轮事件循环:宏任务队列:

第二轮事件循环

​ d定时器当中只有一个console.log('setTimeout')直接输出 setTimeout

输出结束!!

最终结果为:

js 复制代码
输出:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout

这样,我们这道面试题就解决啦!!是不是很简单呢?

不知道大家,看完这篇文章有没有对事件循环机制有一定的理解!希望大家看完能够有所收获!

有哪里不懂或者有异议欢迎大家再评论区留言!!

Coding不易,点个小小的赞鼓励支持一下吧!🥺🥺🥺

个人gitee库:MycodeSpace: 主要应用的仓库,记录学习coding中的点点滴滴 (gitee.com)

相关推荐
逆旅行天涯1 分钟前
【Threejs】从零开始(六)--GUI调试开发3D效果
前端·javascript·3d
小蜗牛慢慢爬行11 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
m0_7482552622 分钟前
easyExcel导出大数据量EXCEL文件,前端实现进度条或者遮罩层
前端·excel
长风清留扬42 分钟前
小程序毕业设计-音乐播放器+源码(可播放)下载即用
javascript·小程序·毕业设计·课程设计·毕设·音乐播放器
web147862107231 小时前
C# .Net Web 路由相关配置
前端·c#·.net
m0_748247801 小时前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
飞的肖1 小时前
前端使用 Element Plus架构vue3.0实现图片拖拉拽,后等比压缩,上传到Spring Boot后端
前端·spring boot·架构
青灯文案11 小时前
前端 HTTP 请求由 Nginx 反向代理和 API 网关到后端服务的流程
前端·nginx·http
ThisIsClark1 小时前
【后端面试总结】MySQL主从复制逻辑的技术介绍
mysql·面试·职场和发展
m0_748254881 小时前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl