摘要:而这些队列由的事件循环来搞定宏任务与微任务,在最新标准中,它们被分别称为与。我们梳理一下事件循环的执行机制循环首先从宏任务开始,遇到,生成执行上下文,开始进入执行栈,可执行代码入栈,依次执行代码,调用完成出栈。
写在前面
js是一门单线程的编程语言,也就是说js在处理任务的时候,所有任务只能在一个线程上排队被执行,那如果某一个任务耗时比较长呢?总不能等到它执行结束再去执行下一个。
所以在线程之内,又被分为了两个队列:
同步任务队列
异步任务队列
举个例子来说:比如你去银行办理业务,都需要领号排队。银行柜员一个个办理业务,这时这个柜员就相当于一个js线程,客户排的队就相当于同步任务队列,每个人对于柜员相当于一个个的任务。
但这个时候,你的电话突然响了,你去接电话接了半小时。这时候人家柜员一看你这情况,直接叫了下一个,而你领的号就作废了,只能重新零号排队。这时候你就是被分发到了异步任务队列。
等你前边的人都完事了,柜员把你叫过去办了你的业务,这时候就是同步队列中的任务执行完了,主线程会处理异步队列中的任务。
这里说的异步任务,它的意思是包含了独立于主执行栈之外的宏任务和微任务。
先看一个简单的例子,对这样的执行机制有个简单的认识:
console.log("start") console.log("end")
上边的执行结果大家肯定都明白,先输出start,再输出end,这一段代码会进入同步队列,顺序执行。
那么我们加点料:
console.log("start") setTimeout(function() { console.log("setTimeout") }, 0) console.log("end")
这样的情况,函数调用栈执行到setTimeout时,setTimeout会在规定的时间点将回调函数放入异步队列,等待同步队列的任务被执行完,立即执行,所以结果是:start、end、setTimeout。
但需要注意的一点是,普遍认为setTimeout定时执行的认知是片面的,因为假设setTimeout规定2秒后执行,但同步队列中有一个函数,执行花了很长时间,甚至花了1秒。那么这时setTimeout中的回调也会等上至少1秒之后,同步任务都执行完了,再去执行。这时候的setTimeout回调执行的时机就会超过2秒,也就是至少3秒。
宏任务与微任务宏任务与微任务都是独立与主执行栈之外的另外两个队列,可以在概念上划分在异步任务队列里。而这些队列由js的事件循环(EventLoop)来搞定
macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
由于写文章时没有注意到,实际上宏任务与微任务的概念是不准确的,但由于文章中涉及多处宏任务、微任务的解读,所以本文暂时还是用宏任务、微任务来分别代指task、jobs。但读者要明白规范中没有宏任务的概念,只有task与jobs
其中宏任务(task)包括:
script(整体代码)
setTimeout, setInterval, setImmediate,
I/O
UI rendering
ajax请求不属于宏任务,js线程遇到ajax请求,会将请求交给对应的http线程处理,一旦请求返回结果,就会将对应的回调放入宏任务队列,等请求完成执行。
微任务(jobs)包括:
process.nextTick
Promise
Object.observe(已废弃)
MutationObserver(html5新特性)
这些我们可以理解为它们在执行上下文中都是可执行代码,会立即执行,只不过会将各自的回调函数放入对应的任务队列中(宏任务微任务),也就相当于一个调度者。
我们梳理一下事件循环的执行机制:
循环首先从宏任务开始,遇到script,生成执行上下文,开始进入执行栈,可执行代码入栈,依次执行代码,调用完成出栈。
执行过程中遇到上边提到的调度者,会同步执行调度者,由调度者将其负责的任务(回调函数)放到对应的任务队列中,直到主执行栈清空,然后开始执行微任务的任务队列。微任务也清空后,再次从宏任务开始,一直循环这一过程。
上边说了那么多,还是用一些代码来验证一下是否是这样的,先来一个简单一点的。
console.log("start") setTimeout(function() { console.log("timeout") }, 0) new Promise(function(resolve) { console.log("promise") resolve() }).then(function() { console.log("promise resolved") }) console.log("end")
根据上边的结论,分析一下执行过程:
建立执行上下文,进入执行栈开始执行代码,打印start
往下执行,遇到setTimeout,将回调函数放入宏任务队列,等待执行
继续往下,有个new Promise,其回调函数并不会被放入其他任务队列,因此会同步地执行,打印promise,但是当resolve后,.then会把其内部的回调函数放入微任务队列
执行到了最底部的代码,打印出end。这时,主执行栈清空了,开始寻找微任务队列里有没有可执行代码
发现了微任务队列中有之前放进去的代码,执行打印出promise resolved,第一次循环结束
再开始第二次循环,从宏任务开始,检查宏任务队列是否有可执行代码,发现有一个,打印timeout
所以,打印顺序是:start-->promise-->end-->promise resolved-->timeout
上边是一个简单示例,比较好理解。那么接下来看一个稍微复杂一点的(这里直接用汉字直观地表明了打印的时机,避免看起来费劲):
console.log("第一次循环主执行栈开始") setTimeout(function() { console.log("第二次循环开始,宏任务队列的第一个宏任务执行中") new Promise(function(resolve) { console.log("宏任务队列的第一个宏任务的微任务继续执行") resolve() }).then(function() { console.log("第二次循环的微任务队列的微任务执行") }) }, 0) new Promise(function(resolve) { console.log("第一次循环主执行栈进行中...") resolve() }).then(function() { console.log("第一次循环微任务,第一次循环结束") setTimeout(function() { console.log("第二次循环的宏任务队列的第二个宏任务执行") }) }) console.log("第一次循环主执行栈完成")
同样我们分析一下执行过程:
第一次循环
进入执行栈执行代码,打印第一次循环主执行栈开始
遇到setTimeout,将回调放入宏任务队列等待执行
promise声明过程是同步的,打印第一次循环主执行栈进行中...,resolve后遇到.then,将回调放入微任务队列
打印第一次循环主执行栈完成
检查微任务队列是否有可执行代码,有一个第三步放入的任务,打印第一次循环微任务,第一次循环结束,第一次循环结束,同时遇到setTimeout,将回调放入宏任务队列
第二次循环
从宏任务入手,检查宏任务队列,发现有两个宏任务,分别是第一次循环第二步和第一次循环第五步被放入的任务,先执行第一个宏任务,打印第二次循环开始,宏任务队列的第一个宏任务执行中
遇到promise声明语句,打印宏任务队列的第一个宏任务继续执行,这时候又被resolve了,又会将.then中的回调放入微任务队列,这是这个宏任务队列中的第一个任务还没执行完
第一个宏任务中的同步代码执行完毕,检查微任务队列,发现有一段第二步放进去的代码,执行打印第二次循环的微任务队列的微任务执行,此时第一个宏任务执行完毕
开始执行第二个宏任务,打印第二次循环的宏任务队列的第二个宏任务执行,所有任务队列全部清空,执行完毕
所以打印顺序为:
第一次循环主执行栈开始
第一次循环主执行栈进行中...
第一次循环主执行栈完成
第一次循环微任务,第一次循环结束
第二次循环开始,宏任务队列的第一个宏任务执行中
第二次循环的宏任务队列的第一个宏任务的微任务继续执行
第二次循环的微任务队列的微任务执行
第二次循环的宏任务队列的第二个宏任务执行
看一下gif,事件循环以肉眼可见的形式呈现出来(两次循环之间有微小的时间间隔)
总结js的执行机制是面试中常考的点,也是非常绕的。但相信完全了解事件循环机制,仔细分析的话,面试遇到这样的题完全不是问题。我在写这篇文章的时候,发现自己之前理解的很大一部分是错的。如果大家觉得哪里有错误,还请帮忙指点出来。
欢迎关注我的公众号: 一口一个前端,不定期分享我所理解的前端知识
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/106108.html
摘要:浏览器是多进程的,而浏览器的内核渲染进程是多线程的。如果已经将回调函数放进任务队列,但是主线程正在执行一个非常耗时的任务,当这个任务执行完毕后,主线程去任务队列中取任务,这个时候,就会出现连续执行的情况,也就是说相当于失效了。 前言 在刷笔试题的时候,经常会碰到setTimeout的问题,只知道这个是设置定时器;但是考察的重点一般是在一个方法中包含了定时器,定时器中的打印和方法中打...
摘要:图片转引自的演讲和两个定时器中回调的执行逻辑便是典型的机制。异步编程关于异步编程我的理解是,在执行环境所提供的异步机制之上,在应用编码层面上实现整体流程控制的异步风格。 问题背景 在一次开发任务中,需要实现如下一个饼状图动画,基于canvas进行绘图,但由于对于JS运行环境中异步机制的不了解,所以遇到了一个棘手的问题,始终无法解决,之后在与同事交流之后才恍然大悟。问题的根节在于经典的J...
摘要:事件触发线程主要负责将准备好的事件交给引擎线程执行。进程浏览器渲染进程浏览器内核,主要负责页面的渲染执行以及事件的循环。第二轮循环结束。 将自己读到的比较好的文章分享出来,大家互相学习,各位大佬有好的文章也可以留个链接互相学习,万分感谢! 线程与进程 关于线程与进程的关系可以用下面的图进行说明: showImg(https://segmentfault.com/img/bVbjSZt?...
阅读 1175·2021-09-04 16:41
阅读 2367·2021-09-02 10:18
阅读 893·2019-08-29 16:40
阅读 2588·2019-08-29 16:14
阅读 876·2019-08-26 13:41
阅读 1278·2019-08-26 12:24
阅读 714·2019-08-26 10:24
阅读 2851·2019-08-23 17:54