摘要:问题引入接触过事件循环的同学大都会纠结一个点,就是在中和执行顺序的随机性。当队列被执行完,或者执行的回调数量达到上限后,事件循环才会进入下一个阶段。嵌套的在下一个事件循环的阶段执行回调输出嵌套的。
问题引入
接触过事件循环的同学大都会纠结一个点,就是在Node中setTimeout和setImmediate执行顺序的随机性。
比如说下面这段代码:
setTimeout(() => { console.log("setTimeout"); }, 0); setImmediate(() => { console.log("setImmediate"); })
执行的结果是这样子的:
为什么会出现这种情况呢?别急,我们先往下看。
浏览器中事件循环模型我们都知道,JavaScript是单线程的语言,对I/O的控制是通过异步来实现的,具体是通过“事件循环”机制来实现。
对于JavaScript中的单线程,指的是JavaScript执行在单线程中,而内部I/O任务其实是另有线程池来完成的。
在浏览器中,我们讨论事件循环,是以“从宏任务队列中取一个任务执行,再取出微任务队列中的所有任务”来分析执行代码的。但是在Node环境中并不适用。具体的浏览器事件循环解析:传送门
在Node中,事件循环的模型和浏览器相比大致相同,而最大的不同点在于Node中事件循环分不同的阶段。具体我们下面会讨论到。本文核心也在这里。
Node中事件循环阶段解析下面是事件循环不同阶段的示意图:
每个阶段都有一个先进先出的回调队列要执行。而每个阶段都有自己的特殊之处。简单来说,就是当事件循环进入某个阶段后,会执行该阶段特定的任意操作,然后才会执行这个阶段里的回调。当队列被执行完,或者执行的回调数量达到上限后,事件循环才会进入下一个阶段。
以下是各个阶段详情。
timers一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定的时间过后,timers会尽早的执行回调,但是系统调度或者其他回调的执行可能会延迟它们。
从技术上来说,poll阶段控制timers什么时候执行,而执行的具体位置在timers。
下限的时间有一个范围:[1, 2147483647],如果设定的时间不在这个范围,将被设置为1。
I/O callbacks这个阶段执行一些系统操作的回调,比如说TCP连接发生错误。
idle, prepare系统内部的一些调用。
poll这是最复杂的一个阶段。
poll阶段有两个主要的功能:一是执行下限时间已经达到的timers的回调,一是处理poll队列里的事件。
注:Node很多API都是基于事件订阅完成的,这些API的回调应该都在poll阶段完成。
以下是Node官网的介绍:
笔者把官网陈述的情况以不同的条件分解,更加的清楚。(如果有误,师请改正。)
当事件循环进入poll阶段:
poll队列不为空的时候,事件循环肯定是先遍历队列并同步执行回调,直到队列清空或执行回调数达到系统上限。
poll队列为空的时候,这里有两种情况。
如果代码已经被setImmediate()设定了回调,那么事件循环直接结束poll阶段进入check阶段来执行check队列里的回调。
如果代码没有被设定setImmediate()设定回调:
如果有被设定的timers,那么此时事件循环会检查timers,如果有一个或多个timers下限时间已经到达,那么事件循环将绕回timers阶段,并执行timers的有效回调队列。
如果没有被设定timers,这个时候事件循环是阻塞在poll阶段等待回调被加入poll队列。
check这个阶段允许在poll阶段结束后立即执行回调。如果poll阶段空闲,并且有被setImmediate()设定的回调,那么事件循环直接跳到check执行而不是阻塞在poll阶段等待回调被加入。
setImmediate()实际上是一个特殊的timer,跑在事件循环中的一个独立的阶段。它使用libuv的API来设定在poll阶段结束后立即执行回调。
注:setImmediate()具有最高优先级,只要poll队列为空,代码被setImmediate(),无论是否有timers达到下限时间,setImmediate()的代码都先执行。
close callbacks如果一个socket或handle被突然关掉(比如socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发。
关于setTimeout和setImmediate代码重现,我们会发现setTimeout和setImmediate在Node环境下执行是靠“随缘法则”的。
比如说下面这段代码:
setTimeout(() => { console.log("setTimeout"); }, 0); setImmediate(() => { console.log("setImmediate"); })
执行的结果是这样子的:
为什么会这样子呢?
这里我们要根据前面的那个事件循环不同阶段的图解来说明一下:
首先进入的是timers阶段,如果我们的机器性能一般,那么进入timers阶段,一毫秒已经过去了(setTimeout(fn, 0)等价于setTimeout(fn, 1)),那么setTimeout的回调会首先执行。
如果没有到一毫秒,那么在timers阶段的时候,下限时间没到,setTimeout回调不执行,事件循环来到了poll阶段,这个时候队列为空,此时有代码被setImmediate(),于是先执行了setImmediate()的回调函数,之后在下一个事件循环再执行setTimemout的回调函数。
而我们在执行代码的时候,进入timers的时间延迟其实是随机的,并不是确定的,所以会出现两个函数执行顺序随机的情况。
那我们再来看一段代码:
var fs = require("fs") fs.readFile(__filename, () => { setTimeout(() => { console.log("timeout"); }, 0); setImmediate(() => { console.log("immediate"); }); });
这里我们就会发现,setImmediate永远先于setTimeout执行。
原因如下:
fs.readFile的回调是在poll阶段执行的,当其回调执行完毕之后,poll队列为空,而setTimeout入了timers的队列,此时有代码被setImmediate(),于是事件循环先进入check阶段执行回调,之后在下一个事件循环再在timers阶段中执行有效回调。
同样的,这段代码也是一样的道理:
setTimeout(() => { setImmediate(() => { console.log("setImmediate"); }); setTimeout(() => { console.log("setTimeout"); }, 0); }, 0);
以上的代码在timers阶段执行外部的setTimeout回调后,内层的setTimeout和setImmediate入队,之后事件循环继续往后面的阶段走,走到poll阶段的时候发现队列为空,此时有代码被setImmedate(),所以直接进入check阶段执行响应回调(注意这里没有去检测timers队列中是否有成员到达下限事件,因为setImmediate()优先)。之后在第二个事件循环的timers阶段中再去执行相应的回调。
综上,我们可以总结:
如果两者都在主模块中调用,那么执行先后取决于进程性能,也就是随机。
如果两者都不在主模块调用(被一个异步操作包裹),那么setImmediate的回调永远先执行。
process.nextTick() and Promise对于这两个,我们可以把它们理解成一个微任务。也就是说,它其实不属于事件循环的一部分。
那么他们是在什么时候执行呢?
不管在什么地方调用,他们都会在其所处的事件循环最后,事件循环进入下一个循环的阶段前执行。
举个?:
setTimeout(() => { console.log("timeout0"); process.nextTick(() => { console.log("nextTick1"); process.nextTick(() => { console.log("nextTick2"); }); }); process.nextTick(() => { console.log("nextTick3"); }); console.log("sync"); setTimeout(() => { console.log("timeout2"); }, 0); }, 0);
结果是:
再解释一下:
timers阶段执行外层setTimeout的回调,遇到同步代码先执行,也就有timeout0、sync的输出。遇到process.nextTick后入微任务队列,依次nextTick1、nextTick3、nextTick2入队后出队输出。之后,在下一个事件循环的timers阶段,执行setTimeout回调输出timeout2。
最后下面给出两段代码,如果能够理解其执行顺序说明你已经理解透彻。
代码1:
setImmediate(function(){ console.log("setImmediate"); setImmediate(function(){ console.log("嵌套setImmediate"); }); process.nextTick(function(){ console.log("nextTick"); }) }); // setImmediate // nextTick // 嵌套setImmediate
解析:事件循环check阶段执行回调函数输出setImmediate,之后输出nextTick。嵌套的setImmediate在下一个事件循环的check阶段执行回调输出嵌套的setImmediate。
代码2:
var fs = require("fs"); function someAsyncOperation (callback) { // 假设这个任务要消耗 95ms fs.readFile("/path/to/file", callback); } var timeoutScheduled = Date.now(); setTimeout(function () { var delay = Date.now() - timeoutScheduled; console.log(delay + "ms have passed since I was scheduled"); }, 100); // someAsyncOperation要消耗 95 ms 才能完成 someAsyncOperation(function () { var startCallback = Date.now(); // 消耗 10ms... while (Date.now() - startCallback < 10) { ; // do nothing } });
解析:事件循环进入poll阶段发现队列为空,并且没有代码被setImmediate()。于是在poll阶段等待timers下限时间到达。当等到95ms时,fs.readFile首先执行了,它的回调被添加进poll队列并同步执行,耗时10ms。此时总共时间累积105ms。等到poll队列为空的时候,事件循环会查看最近到达的timer的下限时间,发现已经到达,再回到timers阶段,执行timer的回调。
如果有什么问题,欢迎留言交流探讨。
参考链接:
https://nodejs.org/en/docs/gu...
https://github.com/creeperyan...
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/107169.html
摘要:异步在中,是在单线程中执行的没错,但是内部完成工作的另有线程池,使用一个主进程和多个线程来模拟异步。在事件循环中,观察者会不断的找到线程池中已经完成的请求对象,从中取出回调函数和数据并执行。 1. 介绍 单线程编程会因阻塞I/O导致硬件资源得不到更优的使用。多线程编程也因为编程中的死锁、状态同步等问题让开发人员头痛。Node在两者之间给出了它的解决方案:利用单线程,远离多线程死锁、状态...
摘要:概述本篇主要介绍的运行机制单线程事件循环结论先在中利用运行至完成和非阻塞完成单线程下异步任务的处理就是先处理主模块主线程上的同步任务再处理异步任务异步任务使用事件循环机制完成调度涉及的内容有单线程事件循环同步执行异步执行定时器的事件循环开始 1.概述 本篇主要介绍JavaScript的运行机制:单线程事件循环(Event Loop). 结论先: 在JavaScript中, 利用运行至...
摘要:事件触发线程主要负责将准备好的事件交给引擎线程执行。它将不同的任务分配给不同的线程,形成一个事件循环,以异步的方式将任务的执行结果返回给引擎。 Fundebug经作者浪里行舟授权首发,未经同意请勿转载。 前言 本文我们将会介绍 JS 实现异步的原理,并且了解了在浏览器和 Node 中 Event Loop 其实是不相同的。 一、线程与进程 1. 概念 我们经常说 JS 是单线程执行的,...
本文涵盖 面试题的引入 对事件循环面试题执行顺序的一些疑问 通过面试题对微任务、事件循环、定时器等对深入理解 结论总结 面试题 面试题如下,大家可以先试着写一下输出结果,然后再看我下面的详细讲解,看看会不会有什么出入,如果把整个顺序弄清楚 Node.js 的执行顺序应该就没问题了。 async function async1(){ console.log(async1 start) ...
阅读 2975·2021-11-23 09:51
阅读 3608·2021-10-13 09:39
阅读 2490·2021-09-22 15:06
阅读 881·2019-08-30 15:55
阅读 3146·2019-08-30 15:44
阅读 1777·2019-08-30 14:05
阅读 3434·2019-08-29 15:24
阅读 2362·2019-08-29 12:44