面试率 90% 的JS事件循环Event Loop,看这篇就够了

我们再来分析下 promise.then(微任务)的处理。当执行到 promise.then 时,V8 引擎不会将异步任务交给浏览器其他线程,而是将回调存在自己的一个队列中,待当前执行栈执行完成后,立马去执行 promise.then 存放的队列,promise.then 微任务没有多线程参与,甚至从某些角度说,微任务都不能完全算是异步,它只是将书写时的代码修改了执行顺序而已。

setTimeout 有“定时等待”这个任务,需要定时器线程执行;ajax 请求有“发送请求”这个任务,需要 HTTP 线程执行,而 promise.then 它没有任何异步任务需要其他线程执行,它只有回调,即使有,也只是内部嵌套的另一个宏任务。

简单小结一下微任务和宏任务的本质区别

宏任务特征:有明确的异步任务需要执行和回调;需要其他异步线程支持。

微任务特征:没有明确的异步任务需要执行,只有回调;不需要其他异步线程支持。

定时器误差


事件循环中,总是先执行同步代码后,才会去任务队列中取出异步回调来执行。当执行 setTimeout 时,浏览器启动新的线程去计时,计时结束后触发定时器事件将回调存入宏任务队列,等待 JS 主线程来取出执行。如果这时主线程还在执行同步任务的过程中,那么此时的宏任务就只有先挂起,这就造成了计时器不准确的问题。同步代码耗时越长,计时器的误差就越大。不仅同步代码,由于微任务会优先执行,所以微任务也会影响计时,假设同步代码中有一个死循环或者微任务中递归不断在启动其他微任务,那么宏任务里面的代码可能永远得不到执行。所以主线程代码的执行效率提升是一件很重要的事情。

在这里插入图片描述

一个很简单的场景就是我们界面上有一个时钟精确到秒,每秒更新一次时间。你会发现有时候秒数会直接跳过 2 秒间隔,就是这个原因。

视图更新渲染


微任务队列执行完成后,也就是一次事件循环结束后,浏览器会执行视图渲染,当然这里会有浏览器的优化,可能会合并多次循环的结果做一次视图重绘,因此视图更新是在事件循环之后,所以并不是每一次操作 Dom 都一定会立马刷新视图。视图重绘之前会先执行 requestAnimationFrame 回调,那么对于 requestAnimationFrame 是微任务还是宏任务是有争议的,在这里看来,它应该既不属于微任务,也不属于宏任务。

四、NodeJS 中的事件循环


JS 引擎本身不实现事件循环机制,这是由它的宿主实现的,浏览器中的事件循环主要是由浏览器来实现,而在 NodeJS 中也有自己的事件循环实现。NodeJS 中也是循环 + 任务队列的流程以及微任务优先于宏任务,大致表现和浏览器是一致的。不过它与浏览器中也有一些差异,并且新增了一些任务类型和任务阶段。接下来我们介绍下NodeJS 中的事件循环流程。

五、NodeJS 中的异步方法


因为都是基于V8 引擎,浏览器中包含的异步方式在 NodeJS 中也是一样的。另外 NodeJS 中还有一些其他常见异步形式。

  • 文件 I/O:异步加载本地文件。

  • setImmediate():与 setTimeout 设置 0ms 类似,在某些同步任务完成后立马执行。

  • process.nextTick():在某些同步任务完成后立马执行。

  • server.close、socket.on(‘close’,…)等:关闭回调。

想象一下,如果上面的形式和 setTimeout、promise 等同时存在,如何分析出代码的执行顺序呢?只要我们理解了 NodeJS 的事件循环机制,也就清楚了。

六、事件循环模型


NodeJS 的跨平台能力和事件循环机制都是基于 Libuv 库实现的,你不用关心这个库的具体内容。我们只需要知道 Libuv 库是事件驱动的,并且封装和统一了不同平台的 API 实现。

NodeJS 中 V8 引擎将 JS 代码解析后调用 Node API,然后 Node API 将任务交给 Libuv 去分配,最后再将执行结果返回给 V8 引擎。在 Libux 中实现了一套事件循环流程来管理这些任务的执行,所以 NodeJS 的事件循环主要是在 Libuv 中完成的。

在这里插入图片描述

事件循环各阶段


在 NodeJS 中 JS 的执行,我们主要需要关心的过程分为以下几个阶段,下面每个阶段都有自己单独的任务队列,当执行到对应阶段时,就判断当前阶段的任务队列是否有需要处理的任务。

  • timers 阶段:执行所有 setTimeout() 和 setInterval() 的回调。

  • pending callbacks 阶段:某些系统操作的回调,如 TCP 链接错误。除 timers、close、setImmediate 的其他大部分回调在此阶段执行。

  • poll 阶段:轮询等待新的链接和请求等事件,执行 I/O 回调等。V8 引擎将 JS 代码解析并传入 Libuv 引擎后首先进入此阶段。如果此阶段任务队列已经执行完了,则进入 check 阶段执行 setImmediate 回调(如果有 setImmediate),或等待新的任务进来(如果没有 setImmediate)。在等待新的任务时,如果有 timers计时到期,则会直接进入 timers 阶段。此阶段可能会阻塞等待。

  • check 阶段:setImmediate 回调函数执行。

  • close callbacks 阶段:关闭回调执行,如 socket.on(‘close’, …)。

在这里插入图片描述

上面每个阶段都会去执行完当前阶段的任务队列,然后继续执行当前阶段的微任务队列,只有当前阶段所有微任务都执行完了,才会进入下个阶段。这里也是与浏览器中逻辑差异较大的地方,不过浏览器不用区分这些阶段,也少了很多异步操作类型,所以不用刻意去区分两者区别。代码如下所示:

const fs = require(‘fs’);

fs.readFile(__filename, (data) => {

// poll(I/O 回调) 阶段

console.log(‘readFile’)

Promise.resolve().then(() => {

console.error(‘promise1’)

})

Promise.resolve().then(() => {

console.error(‘promise2’)

})

});

setTimeout(() => {

// timers 阶段

console.log(‘timeout’);

Promise.resolve().then(() => {

console.error(‘promise3’)

})

Promise.resolve().then(() => {

console.error(‘promise4’)

})

}, 0);

// 下面代码只是为了同步阻塞1秒钟,确保上面的异步任务已经准备好了

var startTime = new Date().getTime();

var endTime = startTime;

while(endTime - startTime < 1000) {

endTime = new Date().getTime();

}

// 最终输出 timeout promise3 promise4 readFile promise1 promise2

另一个与浏览器的差异还体现在同一个阶段里的不同任务执行,在 timers 阶段里面的宏任务、微任务测试代码如下所示:

setTimeout(() => {

console.log(‘timeout1’)

Promise.resolve().then(function() {

console.log(‘promise1’)

})

}, 0);

setTimeout(() => {

console.log(‘timeout2’)

Promise.resolve().then(function() {

console.log(‘promise2’)

})

}, 0);

浏览器中运行 每次宏任务完成后都会优先处理微任务,输出“timeout1”、“promise1”、“timeout2”、“promise2”。

NodeJS 中运行 因为输出 timeout1 时,当前正处于timers阶段,所以会先将所有 timer 回调执行完之后再执行微任务队列,即输出“timeout1”、“timeout2”、“promise1”、“promise2”。

上面的差异可以用浏览器和 NodeJS 10 对比验证。是不是感觉有点反程序员?因此 NodeJS 在版本 11 之后,就修改了此处逻辑使其与浏览器尽量一致,也就是每个 timer 执行后都先去检查一下微任务队列,所以 NodeJS 11 之后的输出已经和浏览器一致了。

nextTick、setImmediate 和 setTimeout


实际项目中我们常用 Promise 或者 setTimeout 来做一些需要延时的任务,比如一些耗时计算或者日志上传等,目的是不希望它的执行占用主线程的时间或者需要依赖整个同步代码执行完成后的结果。

NodeJS 中的 process.nextTick() 和 setImmediate() 也有类似效果。其中 setImmediate() 我们前面已经讲了是在 check 阶段执行的,而 process.nextTick() 的执行时机不太一样,它比 promise.then() 的执行还早,在同步任务之后,其他所有异步任务之前,会优先执行 nextTick。可以想象是把 nextTick 的任务放到了当前循环的后面,与 promise.then() 类似,但比 promise.then() 更前面。意思就是在当前同步代码执行完成后,不管其他异步任务,先尽快执行 nextTick。如下面的代码,因此这里的 nextTick 其实应该更符合“setImmediate”这个命名才对。

setTimeout(() => {

console.log(‘timeout’);

}, 0);

Promise.resolve().then(() => {

console.error(‘promise’)

})

process.nextTick(() => {

console.error(‘nextTick’)

})

// 输出:nextTick、promise、timeout

接下来我们再来看看 setImmediate 和 setTimeout,它们是属于不同的执行阶段了,分别是 timers 阶段和 check 阶段。

setTimeout(() => {

console.log(‘timeout’);

}, 0);

setImmediate(() => {

console.log(‘setImmediate’);

});

// 输出:timeout、 setImmediate

分析上面代码,第一轮循环后,分别将 setTimeout 和 setImmediate 加入了各自阶段的任务队列。第二轮循环首先进入timers 阶段,执行定时器队列回调,然后 pending callbacks和poll 阶段没有任务,因此进入check 阶段执行 setImmediate 回调。所以最后输出为“timeout”、“setImmediate”。当然这里还有种理论上的极端情况,就是第一轮循环结束后耗时很短,导致 setTimeout 的计时还没结束,此时第二轮循环则会先执行 setImmediate 回调。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
img

最后

==
就答题情况而言,第一问100%都可以回答正确,第二问大概只有50%正确率,第三问能回答正确的就不多了,第四问再正确就非常非常少了。其实此题并没有太多刁钻匪夷所思的用法,都是一些可能会遇到的场景,而大多数人但凡有1年到2年的工作经验都应该完全正确才对。
只能说有一些人太急躁太轻视了,希望大家通过此文了解js一些特性。

340)]
[外链图片转存中…(img-YPbonyho-1712054219340)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-pSOQ4ltO-1712054219341)]

最后

==
就答题情况而言,第一问100%都可以回答正确,第二问大概只有50%正确率,第三问能回答正确的就不多了,第四问再正确就非常非常少了。其实此题并没有太多刁钻匪夷所思的用法,都是一些可能会遇到的场景,而大多数人但凡有1年到2年的工作经验都应该完全正确才对。
只能说有一些人太急躁太轻视了,希望大家通过此文了解js一些特性。

并祝愿大家在新的一年找工作面试中胆大心细,发挥出最好的水平,找到一份理想的工作。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值