高频面试题:JavaScript事件循环机制解析
zhezhongyun 2025-01-13 19:11 41 浏览
作者:木子星兮i
转发链接:https://mp.weixin.qq.com/s/KZfEFGnbjQHBcpmI2fnEbg
前言
最近面试了很多家公司,这道题几乎是必被问到的一道题。之前总觉得自己了解得差不多,但是当第一次被问到的时候,却不知道该从哪里开始说起,涉及到的知识点很多。于是花时间整理了一下。并不仅仅是因为面试遇到了,而是理解JavaScript事件循环机制会让我们平常遇到的疑惑也得到解答。
事件循环
- JavaScript是单线程,非阻塞的
- 浏览器的事件循环
- 执行栈和事件队列
- 宏任务和微任务
- node环境下的事件循环
- 和浏览器环境有何不同
- 事件循环模型
- 宏任务和微任务
- 经典题目分析
1. JavaScript是单线程,非阻塞的
单线程:
JavaScript的主要用途是与用户互动,以及操作DOM。如果它是多线程的会有很多复杂的问题要处理,比如有两个线程同时操作DOM,一个线程删除了当前的DOM节点,一个线程是要操作当前的DOM阶段,最后以哪个线程的操作为准?为了避免这种,所以JS是单线程的。即使H5提出了web worker标准,它有很多限制,受主线程控制,是主线程的子线程。
非阻塞:通过 event loop 实现。
2. 浏览器的事件循环
执行栈和事件队列
执行栈: 同步代码的执行,按照顺序添加到执行栈中
事件队列: 异步代码的执行,遇到异步事件不会等待它返回结果,而是将这个事件挂起,继续执行执行栈中的其他任务。当异步事件返回结果,将它放到事件队列中,被放入事件队列不会立刻执行起回调,而是等待当前执行栈中所有任务都执行完毕,主线程空闲状态,主线程会去查找事件队列中是否有任务,如果有,则取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,然后执行其中的同步代码。
img
宏任务和微任务
为什么要引入微任务,只有一种类型的任务不行么?
页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。
不同的异步任务被分为:宏任务和微任务 宏任务:
- script(整体代码)
- setTimeout()
- setInterval()
- postMessage
- I/O
- UI交互事件
微任务:
- new Promise().then(回调)
- MutationObserver(html5 新特性)
运行机制
异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务和微任务队列中去。
在当前执行栈为空时,主线程会查看微任务队列是否有事件存在
- 存在,依次执行队列中的事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的事件,把当前的回调加到当前指向栈。
- 如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;
当前执行栈执行完毕后时会立刻处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
简单总结一下执行的顺序:执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。
宏任务和微任务
node环境下的事件循环
和浏览器环境有何不同
表现出的状态与浏览器大致相同。不同的是 node 中有一套自己的模型。node 中事件循环的实现依赖 libuv 引擎。
事件循环模型
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
事件循环各阶段详解
node中事件循环的顺序
外部输入数据 --> 轮询阶段(poll) --> 检查阶段(check) --> 关闭事件回调阶段(close callback) --> 定时器检查阶段(timer) --> I/O 事件回调阶段(I/O callbacks) --> 闲置阶段(idle, prepare) --> 轮询阶段...
这些阶段大致的功能如下:
- 定时器检测阶段(timers): 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。
- I/O事件回调阶段(I/O callbacks): 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。
- 闲置阶段(idle, prepare): 这个阶段仅在内部使用,可以不必理会
- 轮训阶段(poll): 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
- 检查阶段(check): setImmediate()的回调会在这个阶段执行。
- 关闭事件回调阶段(close callbacks): 例如socket.on('close', ...)这种close事件的回调
poll:这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。check: 该阶段执行setImmediate()的回调函数。
close: 该阶段执行关闭请求的回调函数,比如socket.on('close', ...)。
timer阶段: 这个是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。
I/O callback阶段:除了以下的回调函数,其他都在这个阶段执行:
- setTimeout()和setInterval()的回调函数
- setImmediate()的回调函数
- 用于关闭请求的回调函数,比如socket.on('close', ...)
宏任务和微任务
宏任务:
- setImmediate
- setTimeout
- setInterval
- script(整体代码)
- I/O 操作等。
微任务:
- process.nextTick
- new Promise().then(回调)
Promise.nextTick, setTimeout, setImmediate的使用场景和区别
Promise.nextTick process.nextTick 是一个独立于 eventLoop 的任务队列。在每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行。是所有异步任务中最快执行的。
setTimeout:setTimeout()方法是定义一个回调,并且希望这个回调在我们所指定的时间间隔后第一时间去执行。
setImmediate:setImmediate()方法从意义上将是立刻执行的意思,但是实际上它却是在一个固定的阶段才会执行回调,即poll阶段之后。
经典题目分析
一. 下面代码输出什么
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
先执行宏任务(当前代码块也算是宏任务),然后执行当前宏任务产生的微任务,然后接着执行宏任务
- 从上往下执行代码,先执行同步代码,输出 script start
- 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列中
- 执行 async1(),输出 async1 start, 然后执行 async2(), 输出 async2,把 async2() 后面的代码 console.log('async1 end')放到微任务队列中
- 接着往下执行,输出 promise1,把 .then()放到微任务队列中;注意Promise本身是同步的立即执行函数,.then是异步执行函数
- 接着往下执行, 输出 script end。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码
- 依次执行微任务中的代码,依次输出 async1 end、 promise2, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出 setTimeout
最后的执行结果如下
- script start
- async1 start
- async2
- promise1
- script end
- async1 end
- promise2
- setTimeout
二. 下面代码输出什么
console.log('start');
setTimeout(() => {
console.log('children2');
Promise.resolve().then(() => {
console.log('children3');
})
}, 0);
new Promise(function(resolve, reject) {
console.log('children4');
setTimeout(function() {
console.log('children5');
resolve('children6')
}, 0)
}).then((res) => {
console.log('children7');
setTimeout(() => {
console.log(res);
}, 0)
})
这道题跟上面题目不同之处在于,执行代码会产生很多个宏任务,每个宏任务中又会产生微任务
- 从上往下执行代码,先执行同步代码,输出 start
- 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列①中
- 接着往下执行,输出 children4, 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列②中,此时.then并不会被放到微任务队列中,因为 resolve是放到 setTimeout中执行的
- 代码执行完成之后,会查找微任务队列中的事件,发现并没有,于是开始执行宏任务①,即第一个 setTimeout, 输出 children2,此时,会把 Promise.resolve().then放到微任务队列中。
- 宏任务①中的代码执行完成后,会查找微任务队列,于是输出 children3;然后开始执行宏任务②,即第二个 setTimeout,输出 children5,此时将.then放到微任务队列中。
- 宏任务②中的代码执行完成后,会查找微任务队列,于是输出 children7,遇到 setTimeout,放到宏任务队列中。此时微任务执行完成,开始执行宏任务,输出 children6;
最后的执行结果如下
- start
- children4
- children2
- children3
- children5
- children7
- children6
三. 下面代码输出什么
const p = function() {
return new Promise((resolve, reject) => {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
resolve(2)
})
p1.then((res) => {
console.log(res);
})
console.log(3);
resolve(4);
})
}
p().then((res) => {
console.log(res);
})
console.log('end');
- 执行代码,Promise本身是同步的立即执行函数,.then是异步执行函数。遇到setTimeout,先把其放入宏任务队列中,遇到p1.then会先放到微任务队列中,接着往下执行,输出 3
- 遇到 p().then 会先放到微任务队列中,接着往下执行,输出 end
- 同步代码块执行完成后,开始执行微任务队列中的任务,首先执行 p1.then,输出 2, 接着执行p().then, 输出 4
- 微任务执行完成后,开始执行宏任务,setTimeout, resolve(1),但是此时 p1.then已经执行完成,此时 1不会输出。
最后的执行结果如下
- 3
- end
- 2
- 4
你可以将上述代码中的 resolve(2)注释掉, 此时 1才会输出,输出结果为 3 end 4 1。
const p = function() {
return new Promise((resolve, reject) => {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
})
p1.then((res) => {
console.log(res);
})
console.log(3);
resolve(4);
})
}
p().then((res) => {
console.log(res);
})
console.log('end');
- 3
- end
- 4
- 1
推荐JavaScript学习相关文章
《手写一个Promise/A+ 完美通过官方872个测试用例》
《Node.js 实现抢票小工具&短信通知提醒(上)「干货」》
《Node.js 实现抢票小工具&短信通知提醒(下)「干货」》
《学习 jQuery 源码整体架构,打造属于自己的 js 类库》
《Angular v10.0.0 正式发布,不再支持 IE9/10》
《「实践」浏览器中的画中画(Picture-in-Picture)模式及其 API》
《「多图」一文带你彻底搞懂 Web Workers (上)》
《「多图」一文带你彻底搞懂 Web Workers (中)》
《webpack4主流程源码解说以及动手实现一个简单的webpack(上)》
《webpack4主流程源码解说以及动手实现一个简单的webpack(下)》
《前后端全部用 JS 开发是什么体验(Hybrid + Egg.js经验分享)上》
《前后端全部用 JS 开发是什么体验(Hybrid + Egg.js经验分享)中》
《前后端全部用 JS 开发是什么体验(Hybrid + Egg.js经验分享)下》
《一文带你搞懂 babel-plugin-import 插件(上)「源码解析」》
《一文带你搞懂 babel-plugin-import 插件(下)「源码解析」》
《教你如何使用内联框架元素 IFrames 的沙箱属性提高安全性?》
《细说DOM API中append和appendChild的三个不同点》
《NodeX Component - 滴滴集团 Node.js 生态组件体系「实践」》
《浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务》
《了不起的 Webpack HMR 学习指南(上)「含源码讲解」》
《了不起的 Webpack HMR 学习指南(下)「含源码讲解」》
《图解 Promise 实现原理(二):Promise 链式调用》
《图解 Promise 实现原理(三):Promise 原型方法实现》
《图解 Promise 实现原理(四):Promise 静态方法实现》
《使用Service Worker让你的 Web 应用如虎添翼(上)「干货」》
《使用Service Worker让你的 Web 应用如虎添翼(中)「干货」》
《使用Service Worker让你的 Web 应用如虎添翼(下)「干货」》
《一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索》
《细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」》
《手把手教你7个有趣的JavaScript 项目-上「附源码」》
《手把手教你7个有趣的JavaScript 项目-下「附源码」》
《JavaScript 使用 mediaDevices API 访问摄像头自拍》
《一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法》
《可视化的 JS:动态图演示 - 事件循环 Event Loop的过程》
《可视化的 js:动态图演示 Promises & Async/Await 的过程》
《Pug 3.0.0正式发布,不再支持 Node.js 6/8》
《通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events》
《「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能》
《JavaScript 已进入第三个时代,未来将何去何从?》
《前端上传前预览文件 image、text、json、video、audio「实践」》
《深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系》
《推荐13个有用的JavaScript数组技巧「值得收藏」》
《36个工作中常用的JavaScript函数片段「值得收藏」》
《一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」》
《手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件》
《JavaScript正则深入以及10个非常有意思的正则实战》
《前端开发规范:命名规范、html规范、css规范、js规范》
《100个原生JavaScript代码片段知识点详细汇总【实践】》
《手把手教你深入巩固JavaScript知识体系【思维导图】》
《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》
《身份证号码的正则表达式及验证详解(JavaScript,Regex)》
《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》
《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》
《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》
《面试中教你绕过关于 JavaScript 作用域的 5 个坑》
作者:木子星兮i
转发链接:https://mp.weixin.qq.com/s/KZfEFGnbjQHBcpmI2fnEbg
相关推荐
- 用豆包生成的BMI计算器(豆包的热量是多少?)
-
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8...
- Android 开发中文引导-应用小部件
-
应用小部件是可以嵌入其它应用(例如主屏幕)并收到定期更新的微型应用视图。这些视图在用户界面中被叫做小部件,并可以用应用小部件提供者发布。可以容纳其他应用部件的应用组件叫做应用部件的宿主(1)。下面的截...
- Qt推流(视频文件/视频流/摄像头/桌面转流媒体rtmp+hls+webrtc)
-
一、前言说明推流直播就是把采集阶段封包好的内容传输到服务器的过程。其实就是将现场的视频信号从手机端,电脑端,摄影机端打包传到服务器的过程。“推流”对网络要求比较高,如果网络不稳定,直播效果就会很差,观...
- 一看就会!谷歌广告转化跟踪详细设置指南来了
-
在出海推广业务中,投放广告最常见的目的是获取订单,但我们怎么知道有没有达成投放目的呢?谷歌转化跟踪技术就可以做到!熟悉谷歌的卖家朋友都知道,转化跟踪在最近几年变得越来越复杂了,虽然有很多选项可以自定义...
- Android原生编解码接口MediaCodec详解
-
作者:躬行之MediaCodec是Android中的编解码器组件,用来访问底层提供的编解码器,通常与MediaExtractor、MediaSync、MediaMuxer、MediaCrypt...
- 手把手搭建RTSP流媒体服务器(rtsp 流媒体)
-
0.引言本文主要讲解如何搭建RTSP流媒体服务器的过程,使用开源项目ZLMediaKit。通过这个开源项目,推RTSP流到服务器,然后拉流端可以拉取RTSP、RTMP等流。ZLMediaKit码云链接...
- MediaInfo 24.04.0 是一个关于多媒体文件的信息提供工具
-
MediaInfo24.04.0是一个关于多媒体文件的信息提供工具(仅当文件中包含信息时才提供):包括常规信息(标题、作者、导演、专辑、曲目编号、日期、时长等);视频信息(编解码器、画面比例、帧率...
- rmvb格式视频怎么打开,rmvb转MP4认准这个方法
-
一、rmvb是什么格式? RMVB是一种视频文件格式,其中的VB指的是可变比特率。比起上一代的RM格式,RMVB 格式的画面比较清晰,因为它是降低了静态画面下的比特率。 二、制作rmvb ①...
- 教你用Plex Media Server,把铁威马变成你的“私人好莱坞”!
-
TNAS(铁威马NAS)中可以安装多媒体服务器、影视、PlexMediaServer、EmbyServer作为个人媒体服务器使用。PlexMediaServer可以组织整理TNAS上的媒体...
- 你肯定用过!经典Windows软件被抛弃
-
Windows系统这些年持续更新的过程中,不断融入新的软件和功能的同时,一些经典的应用也渐渐成为了历史……Windows媒体播放器被抛弃Windows系统不断地推陈出新,一些老旧的组件也难免被抛弃,在...
- 博思得Q8标签打印全能手(博思得标签打印机安装教程)
-
2014-12-0905:35:00作者:宋达希【中关村在线办公打印频道原创】服装吊牌、洗涤标签、产品说明标签等都要用到标签打印机,这些标签涵盖多种尺寸的长度和宽度以及材质。另外作为一件商品或者产...
- flv文件用什么播放器打开,这样做不踩雷!
-
FLV是FLASHVIDEO的简称,是随着FlashMX的推出发展而来的视频格式。它的出现有效地解决了视频文件导入Flash后,使导出的SWF文件体积庞大,不能在网络上很好的使用等问题。一、...
- media player怎么转换格式?音频转换神器推荐!
-
Windowsmediaplayer怎么转换格式?WindowsMediaPlayer是微软公司出品的一款多媒体播放器,通常简称“WMP”。提供了编辑音频和视频文件的功能。用户可以使用该软件导...
- 视频参数检查工具更新:MediaInfo 23.10
-
MediaInfo提供有关视频或音频文件的技术和标签信息。信息示例包括编解码器、比特率、每秒帧数、宽度、高度、频道数、持续时间、标题、作者、字幕语言和章节名称。多种方式可以查看信息(文本、工作表、树和...
- 多媒体管理软件:JRiver Media Center 31.0.68 (64位)
-
JRiverMediaCenter64位是适用于大量库的完整媒体解决方案。它组织、播放和标记所有类型的媒体文件,并对Xbox、PS3、UPnP、DLNA和TiVo进行翻录、刻录。JRiverM...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML常用标签 (29)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- HTML button formtarget 属性 (30)
- CSS 水平对齐 (Horizontal Align) (30)