前端基础进阶(十四):深入核心,详解事件循环机制
yuyutoo 2025-05-25 16:23 3 浏览 0 评论
Event Loop
JavaScript的学习零散而庞杂,很多时候我们学到了一些东西,但是却没办法感受到进步!甚至过了不久,就把学到的东西给忘了。为了解决自己的这个困扰,在学习的过程中,我一直在试图寻找一条核心的线索,只要顺着这条线索,我就能够一点一点的进步。
前端基础进阶正是围绕这条线索慢慢展开,而事件循环机制(Event Loop),则是这条线索的最关键的知识点。
所以,我就马不停蹄的去深入的学习了事件循环机制,并总结出了这篇文章跟大家分享。
1
事件循环机制从整体上告诉了我们JavaScript代码的执行顺序。但是在我学习的过程中,找到的许多国内博客文章对于它的讲解浅尝辄止,不得其法,很多文章在图中画个圈就表示循环了,看了之后也没感觉明白了多少。但是他又如此重要,以致于当我想要面试中高级岗位时,事件循环机制总是绕不开的话题。特别是ES6中正式加入了Promise对象之后,对于新标准中事件循环机制的理解就变得更加重要。这就很尴尬了。
最近有两篇比较火的文章也表达了这个问题的重要性。
这个前端面试在搞事
80% 应聘者都不及格的 JS 面试题
但很遗憾的是,大神们告诉了大家这个知识点很重要,却并没有告诉大家为什么会这样。所以当我们在面试遇到这样的问题时,就算你知道了结果,面试官再进一步问一下,我们依然懵逼。
学习事件循环机制之前,我默认你已经懂得了如下概念
- 执行上下文(Execution context)
- 函数调用栈(call stack)
- 队列数据结构(queue)
- Promise(我会在下一篇文章专门总结Promise的详细使用)
因为chrome浏览器中新标准中的事件循环机制与nodejs类似,因此此处就整合nodejs一起来理解,其中会介绍到几个nodejs有,但是浏览器中没有的API,大家只需要了解就好,不一定非要知道她是如何使用。比如process.nextTick,setImmediate
OK,那我就先抛出结论,然后以例子与图示详细给大家演示事件循环机制。
我们知道JavaScript的一大特点就是单线程,而这个线程中拥有唯一的一个事件循环。
当然新标准中的web worker涉及到了多线程,这里就不讨论了。
JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。
队列数据结构
- 一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。
- 任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
- macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
- micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
- setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
// setTimeout中的回调函数才是进入任务队列的任务
setTimeout(function() {
console.log('xxxx');
})
// 非常多的同学对于setTimeout的理解存在偏差。所以大概说一下误解:
// setTimeout作为一个任务分发器,这个函数会立即执行,而它所要分发的任务,也就是它的第一个参数,才是延迟执行
- 来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
- 事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
- 其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。
纯文字表述确实有点干涩,因此,这里我们通过2个例子,来逐步理解事件循环的具体顺序。
// demo01 出自于上面我引用文章的一个例子,我们来根据上面的结论,一步一步分析具体的执行过程。
// 为了方便理解,我以打印出来的字符作为当前的任务名称
setTimeout(function() {
console.log('timeout1');
})
new Promise(function(resolve) {
console.log('promise1');
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2');
}).then(function() {
console.log('then1');
})
console.log('global1');
首先,事件循环从宏任务队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务。每一个任务的执行顺序,都依靠函数调用栈来搞定,而当遇到任务源时,则会先分发任务到对应的队列中去,所以,上面例子的第一步执行如下图所示。
首先script任务开始执行,全局上下文入栈
第二步:script任务执行时首先遇到了setTimeout,setTimeout为一个宏任务源,那么他的作用就是将任务分发到它对应的队列中。
setTimeout(function() {
console.log('timeout1');
})
宏任务timeout1进入setTimeout队列
第三步:script执行时遇到Promise实例。Promise构造函数中的第一个参数,是在new的时候执行,因此不会进入任何其他的队列,而是直接在当前任务直接执行了,而后续的.then则会被分发到micro-task的Promise队列中去。
因此,构造函数执行时,里面的参数进入函数调用栈执行。for循环不会进入任何队列,因此代码会依次执行,所以这里的promise1和promise2会依次输出。
promise1入栈执行,这时promise1被最先输出
resolve在for循环中入栈执行
构造函数执行完毕的过程中,resolve执行完毕出栈,promise2输出,promise1页出栈,then执行时,Promise任务then1进入对应队列
script任务继续往下执行,最后只有一句输出了globa1,然后,全局任务就执行完毕了。
第四步:第一个宏任务script执行完毕之后,就开始执行所有的可执行的微任务。这个时候,微任务中,只有Promise队列中的一个任务then1,因此直接执行就行了,执行结果输出then1,当然,他的执行,也是进入函数调用栈中执行的。
执行所有的微任务
第五步:当所有的micro-tast执行完毕之后,表示第一轮的循环就结束了。这个时候就得开始第二轮的循环。第二轮循环仍然从宏任务macro-task开始。
微任务被清空
这个时候,我们发现宏任务中,只有在setTimeout队列中还要一个timeout1的任务等待执行。因此就直接执行即可。
timeout1入栈执行
这个时候宏任务队列与微任务队列中都没有任务了,所以代码就不会再输出其他东西了。
那么上面这个例子的输出结果就显而易见。大家可以自行尝试体会。
这个例子比较简答,涉及到的队列任务并不多,因此读懂了它还不能全面的了解到事件循环机制的全貌。所以我下面弄了一个复杂一点的例子,再给大家解析一番,相信读懂之后,事件循环这个问题,再面试中再次被问到就难不倒大家了。
// demo02
console.log('golb1');
setTimeout(function() {
console.log('timeout1');
process.nextTick(function() {
console.log('timeout1_nextTick');
})
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
})
setImmediate(function() {
console.log('immediate1');
process.nextTick(function() {
console.log('immediate1_nextTick');
})
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then')
})
})
process.nextTick(function() {
console.log('glob1_nextTick');
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
setTimeout(function() {
console.log('timeout2');
process.nextTick(function() {
console.log('timeout2_nextTick');
})
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
process.nextTick(function() {
console.log('glob2_nextTick');
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})
setImmediate(function() {
console.log('immediate2');
process.nextTick(function() {
console.log('immediate2_nextTick');
})
new Promise(function(resolve) {
console.log('immediate2_promise');
resolve();
}).then(function() {
console.log('immediate2_then')
})
})
这个例子看上去有点复杂,乱七八糟的代码一大堆,不过不用担心,我们一步一步来分析一下。
第一步:宏任务script首先执行。全局入栈。glob1输出。
script首先执行
第二步,执行过程遇到setTimeout。setTimeout作为任务分发器,将任务分发到对应的宏任务队列中。
setTimeout(function() {
console.log('timeout1');
process.nextTick(function() {
console.log('timeout1_nextTick');
})
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
})
timeout1进入对应队列
第三步:执行过程遇到setImmediate。setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中。setImmediate的任务队列会在setTimeout队列的后面执行。
setImmediate(function() {
console.log('immediate1');
process.nextTick(function() {
console.log('immediate1_nextTick');
})
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then')
})
})
进入setImmediate队列
第四步:执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去。
process.nextTick(function() {
console.log('glob1_nextTick');
})
nextTick
第五步:执行遇到Promise。Promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法会直接执行。因此,glob1_promise会第二个输出。
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
先是函数调用栈的变化
然后glob1_then任务进入队列
第六步:执行遇到第二个setTimeout。
setTimeout(function() {
console.log('timeout2');
process.nextTick(function() {
console.log('timeout2_nextTick');
})
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
timeout2进入对应队列
第七步:先后遇到nextTick与Promise
process.nextTick(function() {
console.log('glob2_nextTick');
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})
glob2_nextTick与Promise任务分别进入各自的队列
第八步:再次遇到setImmediate。
setImmediate(function() {
console.log('immediate2');
process.nextTick(function() {
console.log('immediate2_nextTick');
})
new Promise(function(resolve) {
console.log('immediate2_promise');
resolve();
}).then(function() {
console.log('immediate2_then')
})
})
nextTick
这个时候,script中的代码就执行完毕了,执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去。接下来,将会执行所有的微任务队列中的任务。
其中,nextTick队列会比Promie先执行。nextTick中的可执行任务执行完毕之后,才会开始执行Promise队列中的任务。
当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了。下一轮循环继续从宏任务队列开始执行。
这个时候,script已经执行完毕,所以就从setTimeout队列开始执行。
第二轮循环初始状态
setTimeout任务的执行,也依然是借助函数调用栈来完成,并且遇到任务分发器的时候也会将任务分发到对应的队列中去。
只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。
setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务。
当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了。
大家需要注意这里的循环结束的时间节点。
当我们在执行setTimeout任务中遇到setTimeout时,它仍然会将对应的任务分发到setTimeout队列中去,但是该任务就得等到下一轮事件循环执行了。例子中没有涉及到这么复杂的嵌套,大家可以动手添加或者修改他们的位置来感受一下循环的变化。
OK,到这里,事件循环我想我已经表述得很清楚了,能不能理解就看读者老爷们有没有耐心了。我估计很多人会理解不了循环结束的节点。
当然,这些顺序都是v8的一些实现。我们也可以根据上面的规则,来尝试实现一下事件循环的机制。
// 用数组模拟一个队列
var tasks = [];
// 模拟一个事件分发器
var addFn1 = function(task) {
tasks.push(task);
}
// 执行所有的任务
var flush = function() {
tasks.map(function(task) {
task();
})
}
// 最后利用setTimeout/或者其他你认为合适的方式丢入事件循环中
setTimeout(function() {
flush();
})
// 当然,也可以不用丢进事件循环,而是我们自己手动在适当的时机去执行对应的某一个方法
var dispatch = function(name) {
tasks.map(function(item) {
if(item.name == name) {
item.handler();
}
})
}
// 当然,我们把任务丢进去的时候,多保存一个name即可。
// 这时候,task的格式就如下
demoTask = {
name: 'demo',
handler: function() {}
}
// 于是,一个订阅-通知的设计模式就这样轻松的被实现了
这样,我们就模拟了一个任务队列。我们还可以定义另外一个队列,利用上面的各种方式来规定他们的优先级。
需要注意的是,这里的执行顺序,或者执行的优先级在不同的场景里由于实现的不同会导致不同的结果,包括node的不同版本,不同浏览器等都有不同的结果。
建议收藏,不然刷着刷着就可能找不到了。
有什么前端的问题欢迎私信我~期待你的到来。
我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年我花了一个月整理了一份最适合2020年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取
相关推荐
- 12、高阶组件:魔法增幅器——React 19 HOC模式
-
一、魔法增幅器的本质"高阶组件是魔法师用咒语叠加的炼金术,"霍格沃茨魔咒研究院院长凝视着发光的增幅器,"通过函数式能量场的嵌套,让基础组件获得预言家日报式的逻辑继承!"...
- 深入理解nodejs的异步IO与事件模块机制
-
一、node为什么要使用异步I/O异步最先诞生于操作系统的底层,在底层系统中,异步通过信号量、消息等方式有广泛的应用。但在大多数高级编程语言中,异步并不多见,这是因为编写异步的程序不符合人习惯的思维逻...
- 前端时间同步利器:React + useEffect 实现高性能动态时钟
-
前言在你奋笔疾敲代码的瞬间,是不是突然一低头,发现时间像偷偷跑路的变量,一眨眼就从上午飘到下午?饭没吃、会没开、工位也快被前端猫霸占了。仿佛你写的不是代码,而是“时间穿梭机”。别慌,咱们今天就来用R...
- JavaScript 异步编程指南 - 聊聊 Node.js 中的事件循环
-
作者:五月君来源:编程界|事件循环是一种控制应用程序的运行机制,在不同的运行时环境有不同的实现,上一节讲了浏览器中的事件循环,它们有很多相似的地方,也有着各自的特点,本节讨论下Node.js中...
- 10个Vue开发技巧「实践」
-
作者:WahFung转发链接:https://juejin.im/post/5e8a9b1ae51d45470720bdfa路由参数解耦一般在组件内使用路由参数,大多数人会这样做:...
- 通过番计时器实例学习 React 生命周期函数 componentDidMount
-
大家好,今天我们将通过一个实例——番茄计时器,学习下如何使用函数生命周期的一个重要函数componentDidMount():componentDidMount(),在组件加载完成,render之后...
- 前端必看!10 个 Vue3 救命技巧,解决你 90% 的开发难题?
-
写Vue3项目时,是不是总被数据更新延迟、组件间传值混乱、页面加载缓慢这些问题折磨得头秃?别担心!作为摸爬滚打多年的老前端,今天掏出压箱底的10个实战技巧,从性能优化到复杂逻辑处理,每一个都能...
- 如何用2 KB代码实现3D赛车游戏?2kPlus Jam大赛了解一下
-
选自frankforce作者:Frank机器之心编译参与:王子嘉、GeekAI控制复杂度一直是软件开发的核心问题之一,一代代的计算机从业者纷纷贡献着自己的智慧,试图降低程序的计算复杂度。然而,将一款...
- 证明你访问的网站是你想访问的,Safari 真的需要
-
安全研究员在Safari上找到了一个新漏洞,能让网站在浏览器的地址栏内将自己伪装成另一个网站——得益于Safari地址栏的“智能缩略”功能。在Deusen最近公开的攻击演示(PoC,P...
- 抓狂!TS 组件性能拉胯到崩溃?4 个绝杀技巧逆风翻盘!
-
前端兄弟姐妹们五一假期快乐,咱们谁还没被TypeScript组件的性能问题折磨过?页面加载转圈圈,点击按钮没反应,代码改了一轮又一轮,性能却还是原地踏步,分分钟想砸电脑!别慌,今天这4个绝杀技...
- 让小球做圆周运动,你有几种办法?
-
最近在阅读外国技术文章中无意中发现了一个神奇的CSS属性motion-path,它可以让Dom元素可以按照自定义的路径移动。又想起了很久之前参加校招面试的时候,面试官问了我一个问题“能不能不借助库实现...
- 前端基础进阶(十四):深入核心,详解事件循环机制
-
EventLoopJavaScript的学习零散而庞杂,很多时候我们学到了一些东西,但是却没办法感受到进步!甚至过了不久,就把学到的东西给忘了。为了解决自己的这个困扰,在学习的过程中,我一直在试图寻...
- 从0搭建一个WebRTC,实现多房间多对多通话,并实现屏幕录制
-
这篇文章开始会实现一个一对一WebRTC和多对多的WebRTC,以及基于屏幕共享的录制。本篇会实现信令和前端部分,信令使用fastity来搭建,前端部分使用Vue3来实现。为什么要使用WebRTCWe...
- Vue2 开发卡壳?这 10 个实战技巧专治各种不服
-
干前端开发的兄弟,谁还没被Vue2折腾过?数据不更新、组件通信乱成麻、性能差到想砸电脑……这些痛点,我都懂!今天直接甩出10个超实用的实战技巧,每一个都是从项目“血坑”里爬出来总结的,专...
你 发表评论:
欢迎- 一周热门
-
-
前端面试:iframe 的优缺点? iframe有那些缺点
-
带斜线的表头制作好了,如何填充内容?这几种方法你更喜欢哪个?
-
漫学笔记之PHP.ini常用的配置信息
-
其实模版网站在开发工作中很重要,推荐几个参考站给大家
-
推荐7个模板代码和其他游戏源码下载的网址
-
[干货] JAVA - JVM - 2 内存两分 [干货]+java+-+jvm+-+2+内存两分吗
-
正在学习使用python搭建自动化测试框架?这个系统包你可能会用到
-
织梦(Dedecms)建站教程 织梦建站详细步骤
-
【开源分享】2024PHP在线客服系统源码(搭建教程+终身使用)
-
2024PHP在线客服系统源码+完全开源 带详细搭建教程
-
- 最近发表
-
- 12、高阶组件:魔法增幅器——React 19 HOC模式
- 深入理解nodejs的异步IO与事件模块机制
- 前端时间同步利器:React + useEffect 实现高性能动态时钟
- JavaScript 异步编程指南 - 聊聊 Node.js 中的事件循环
- 10个Vue开发技巧「实践」
- 通过番计时器实例学习 React 生命周期函数 componentDidMount
- SRE监控四大黄金指标,任何一个有异常都会是灾难……
- 前端必看!10 个 Vue3 救命技巧,解决你 90% 的开发难题?
- 如何用2 KB代码实现3D赛车游戏?2kPlus Jam大赛了解一下
- 证明你访问的网站是你想访问的,Safari 真的需要
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)