深入理解Javascript中的event loop(事件循环)机制

面试的时候,可能有的面试官问我们:"可以介绍一下JavaScript事件循环机制吗?"或者"对微任务和宏任务有了解吗?"

再或者人家也懒得问我们,直接把下面这道面试题甩给我们,让我们写出它最后的执行结果;

js 复制代码
console.log('1')

setTimeout(function callback(){
	console.log('2')
}, 1000)

new Promise((resolve, reject) => {
    console.log('3')
    resolve()
})
.then(res => {
    console.log('4');
})

console.log('5')

输出是:1,3,5,4,2

看看和你想的一样不,然后接下来,我们就一起来研究为什么是上面的打印结果;

上面的三种行为其实就是考察我们对event loop(事件循环)机制的认识;

那今天我们就围绕这个问题来探索一下,看完回来,你就能深入的理解事件循环机制

JavaScript是单线程

JavaScript是一门单线程的编程语言,意思就是同一时间段只能做一件事,所有任务都需要排队依次完成;

那么,为什么JavaScript不能有多个线程呢?

答:作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准;

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

同步任务和异步任务

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

我们把上面得部分代码拿过来,如下:

js 复制代码
console.log('1')

setTimeout(function (){
	console.log('2')
}, 1000)

console.log('3')

/* 运行结果:
	1
	3
	2
*/

这时就有人纳闷啦,Js不是单线程吗,它应该从上到下一行一行执行的,只有当上一行的代码执行完后才会执行下一行代码,那应该打印出:1,2,3;那为什么是:1,3,2呢;

JavaScript开发人员意识到,为了不影响主线程正常运行,就把那些耗时的时间(比如定时器,Ajax操作从网络读取数据等)任务挂起来,依次的放进一个任务队列中,等主线程的任务执行完毕后,再回过来去继续执行队列中的任务;

于是,任务就可以分成两种:

  • 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  • 异步任务:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

event loop(事件循环)

其实上面的代码执行动图大致如下:

执行机制过程如下:

  • 所有同步任务都在主线程上执行,形成一个执行栈(调用栈);

  • 主线程之外,还存在一个'任务队列'(task queue),浏览器中的各种 Web API 为异步的代码提供了一个单独的运行空间,当异步的代码运行完毕以后,会将代码中的回调送入到 任务队列中(队列遵循先进先出得原则)

  • 一旦主线程的栈中的所有同步任务执行完毕后,调用栈为空时系统就会将队列中的回调函数依次压入调用栈中执行,当调用栈为空时,仍然会不断循环检测任务队列中是否有代码需要执行;

其实,这一过程就是我们要了解的event loop(事件循环)机制;

现在你知道为什么打印出来的结果是:1、3、2了吧;

宏任务和微任务

回过头来我们再看看这道面试题:

js 复制代码
console.log('1')

setTimeout(function callback(){
	console.log('2')
}, 1000)

new Promise((resolve, reject) => {
    console.log('3')
    resolve()
})
.then(res => {
    console.log('4');
})

console.log('5')

扩展:这里涉及到 Promise构造函数,这里就不多讲;这里大家知道Promise函数是同步执行,.then方法为异步执行就可以;

上面说过了同步和异步任务,也了解了event loop(事件循环)机制,大家知道要先等调用栈的同步任务执行完毕后再去执行异步任务,所以先打印出:1、3、5,这是没有问题,setTimeout()和Promise()都是异步任务,那接下来setTimeout()先于Promise()插入异步任务队列,为什么先打印的是4,不是2呢?

实际上,异步任务也有区别,分为:宏任务 和 微任务

浏览器中常用的宏任务和微任务:

名称 事件
宏任务 setTimeout 、setInterval 、Ajax
微任务 Promise、async/await

那么异步任务既然分为宏任务和微任务,则队列肯定也分为宏任务队列和微任务队列啦;

当宏任务和微任务都处于 任务队列(Task Queue) 中时,微任务的优先级大于宏任务,即先将微任务执行完,再执行宏任务;

其实上面得代码执行动图大致如下:

执行机制过程如下:

第一步: 主线程执行同步任务的同时,把一些异步任务放入'任务队列'(task queue)中,等待主线程的调用栈为空时,再依次从队列出去任务去执行;

第二步:检测任务队列中的微队列是否为空,若不为空,则取出一个微任务入栈执行;然后继续执行第2步;如果微队列为空,则开始取出宏队列中的一个宏任务执行;

第三步:执行完宏队列中的一个宏任务后,会继续检测微队列是否为空,如果有新插入的任务,这继续执行第二步;如果微队列为空,则继续执行宏队列中的下一个任务,然后再继续循环执行第三步;

上面的执行步骤可以结合动图来看,一定要看懂,一定要理解!

现在知道了为什么打印出:1,3,5,4,2 了吧;

检测练习

为了看你是否真正理解,可以看下面代码,打印输出为?

js 复制代码
setTimeout( () => {
  console.log('1')
  Promise.resole().then( () => {
    console.log('2')
  })
},0)

new Promise((resolve, reject) => {
    console.log('3')
    resolve()
})
.then(res => {
    console.log('4');
})

console.log('5')
相关推荐
触底反弹11 分钟前
一文彻底搞懂 JavaScript 栈和队列(建议收藏)
javascript·算法·面试
To_OC12 分钟前
我一直以为 Ajax 是个黑盒,直到我写了这 50 行代码
前端·后端·全栈
用户0595401744617 分钟前
RAG 记忆层踩坑实录:用户偏好凭空消失,我排查了 4 小时,最后用 LangChain + Chroma 搭了套自动化回归测试
前端·css
Asize20 分钟前
Prompt 驱动 NLP:从 ES6 模块化到文本推理实战
javascript·人工智能·机器学习
程序猿阿伟23 分钟前
《Chrome隔离机制的维度落地指南》
前端·chrome
用户0543243297025 分钟前
AI 生成的代码怎么在前端安全预览 + 一键运行:sandbox iframe 实战 🔒
前端
ALianBlank26 分钟前
一个 Unity 框架能做多少事?86 个模块 + 21 个小游戏平台
前端·后端·游戏开发
JieE21227 分钟前
树与二叉树--JS实例
javascript·数据结构
To_OC29 分钟前
搞懂二叉树递归遍历,我居然是从爬楼梯开始的
前端·javascript·数据结构
何何____34 分钟前
svg基本图形绘制介绍
前端·css