面试之 Event Loop,nextTick() 和 setImmediate() 区别分析
序
一直以来,我对 Event Loop 的认知界定都是可知可不知的分级,因此仅仅保留浅显的概念,从未真正学习过,直到看了这篇文章:这一次,彻底弄懂 JavaScript 执行机制。该文作者写的非常友好,从最小的例子展开,让我获益匪浅,但最后的示例牵扯出了chrome
和Node
下的运行结果迥异,我很好奇,我觉得有必要对这一块知识进行学习。
由于上述原因,本文诞生,原本我计划全文共分3部分来展开:规范、实现、应用。但遗憾的是由于自己的认知尚浅,在如何根据 Event Loop 的特性来设想应用场景时,实在没有什么产出,导致有关应用的篇幅过小,故不在标题中作体现了。
本文所有代码运行环境仅包含 Node v8.9.4 以及 Chrome v63
PART 1:规范
为什么要有 Event Loop?
因为 JavaScript 设计之初就是一门单线程语言,因此为了实现主线程的不阻塞,Event Loop 这样的方案应运而生。
小测试(1)
先来看一段代码,打印结果会是?
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then(() => {
console.log(3)
}).then(() => {
console.log(4)
})
console.log(5)
不熟悉 Event Loop 的我尝试进行如下分析:
- 首先,我们先排除异步代码,先把同步执行的代码找出,可以知道先打印的一定是
1、5
- 但是,setTimeout 和 Promise 是否有优先级?还是看执行顺序?
- 还有,Promise 的多级 then 之间是否会插入 setTimeout?
带着困惑,我试着运行了一下代码,正确结果是:1、5、3、4、2
。
那这到底是为什么呢?
定义
看来需要先从规范定义入手,于是查阅一下 HTML规范,规范着实详(luo)细(suo),我就不贴了,提炼下来关键步骤如下:
- 执行最旧的 task(一次)
- 检查是否存在 microtask,然后不停执行,直到清空队列(多次)
- 执行 render
好家伙,问题还没搞明白,一下子又多出来2个概念 task 和 microtask,让懵逼的我更加凌乱了。不慌不慌,通过仔细阅读文档得知,这两个概念属于对异步任务的分类,不同的API注册的异步任务会依次进入自身对应的队列中,然后等待Event Loop将它们依次压入执行栈中执行。
task 主要包含:
setTimeout
、setInterval
、setImmediate
、I/O
、UI 交互事件
microtask 主要包含:Promise
、process.nextTick
、MutaionObserver
整个最基本的 Event Loop 如图所示:
- queue 可以看做一种数据结构,用以存储需要执行的函数
- timer 类型的 API(setTimeout/setInterval)注册的函数,等到期后进入 task 队列(这里不详细展开 timer 的运行机制)
- 其余 API 注册函数直接进入自身对应的 task/microtask 队列
- Event Loop 执行一次,从 task 队列中拉出一个 task 执行
- Event Loop 继续检查 microtask 队列是否为空,依次执行直至清空队列
继续测试(2)
这时候,回头再看下之前的 测试(1)
,发现概念非常清晰,一下子就得出了正确答案,感觉自己萌萌哒,再也不怕 Event Loop 了~
接着,准备挑战一下更高难度的问题(本题出自序中提到的那篇文章,我先去除了 process.nextTick:
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
setTimeout(() => {
console.log(9)
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
分析如下:
- 同步运行的代码首先输出:
1、7
- 接着,清空microtask队列:
8
- 第一个task执行:
2、4
- 接着,清空microtask队列:
5
- 第二个task执行:
9、11
- 接着,清空microtask队列:
12
在chrome
下运行一下,全对!
自信的我膨胀了,准备加上process.nextTick
后在 node 上继续测试。我先测试第一个 task,代码如下:
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
process.nextTick(() => {
console.log(3)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
process.nextTick(() => {
console.log(6)
})
有了之前的积累,我这回自信的写下了答案:1、7、8、6、2、4、5、3
。
然而,帅不过3秒,正确答案是:1、7、6、8、2、4、3、5
。
我陷入了困惑,不过很快明白了,这说明 process.nextTick
注册的函数优先级高于 Promise
,这样就全说的通了~
接着,我再测试第二个 task:
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
process.nextTick(() => {
console.log(3)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
process.nextTick(() => {
console.log(6)
})
setTimeout(() => {
console.log(9)
process.nextTick(() => {
console.log(10)
})
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
吃一堑长一智,这次我掌握了microtask的优先级,所以答案应该是:
- 第一个task输出:
1、7、6、8、2、4、3、5
- 然后,第二个task输出:
9、11、10、12
然而,啪啪打脸。我第一次执行,输出结果是:1、7、6、8、2、4、9、11、3、10、5、12
(即两次task的执行混合在一起了)。我继续执行,有时候又会输出我预期的答案。现实真的是如此莫名啊!啊!啊!
(啊,不好意思,血一时止不住)所以,这到底是为什么???
PART 2:实现
俗话说得好
规范是人定的,代码是人写的。 ——无名氏
规范无法囊括所有场景,虽然
chrome
和node
都基于v8引擎,但引擎只负责管理内存堆栈,API还是由各runtime自行设计并实现的。
小测试(3)
Timer 是整个 Event Loop 中非常重要的一环,我们先从 timer 切入,来切身体会下规范和实现的差异。
setTimeout(() => {
console.log(2)
}, 2)
setTimeout(() => {
console.log(1)
}, 1)
setTimeout(() => {
console.log(0)
}, 0)
没有深入接触过 timer 的同学如果直接从代码中的延时设置来看,会回答:0、1、2
。
而另一些有一定经验的同学可能会回答:2、1、0
。因为MDN 的 setTimeout 文档中提到 HTML 规范最低延时为 4ms:
补充说明:最低延时的设置是为了给 CPU 留下休息时间
In fact, 4ms is specified by the HTML5 spec and is consistent across browsers released in 2010 and onward. Prior to (Firefox 5.0 / Thunderbird 5.0 / SeaMonkey 2.2), the minimum timeout value for nested timeouts was 10 ms.
而真正痛过的同学会告诉你,答案是:
1、0、2
。并且,无论是chrome
还是node
下的运行结果都是一致的。
Chrome 中的 timer
从测试(3)
结果可以看出,0ms和1ms的延时效果是一致的,那背后的原因是为什么呢?我们先查查blink
的实现。
Blink 代码托管的地方我都不知道如何进行搜索,还好文件名比较明显,没花太久,找到了答案
我直接贴出最底层代码,上层代码如有兴趣请自行查阅
// https://chromium.googlesource.com/chromium/blink/+/master/Source/core/frame/DOMTimer.cpp#93
double intervalMilliseconds = std::max(oneMillisecond, interval * oneMillisecond);
这里interval就是传入的数值,可以看出传入0和传入1结果都是oneMillisecond,即1ms。
这样解释了为何1ms和0ms行为是一致的,那4ms到底是怎么回事?我再次确认了HTML规范,发现虽然有4ms的限制,但是是存在条件的,详见规范第11点:
If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.
并且有意思的是,MDN英文文档的说明也已经贴合了这个规范。
我斗胆推测,一开始HTML5规范确实有定最低4ms的规范,不过在后续修订中进行了修改,我认为甚至不排除规范在向实现看齐,即逆向影响。
Node 中的 timer
那node
中,为什么 0ms 和 1ms 的延时效果一致呢?
还是 github 托管代码看起来方便,直接搜到目标代码
// https://github.com/nodejs/node/blob/v8.9.4/lib/timers.js#L456
if (!(after >= 1 && after <= TIMEOUT_MAX))
after = 1; // schedule on next tick, follows browser behavior
代码中的注释直接说明了,设置最低1ms的行为是为了向浏览器行为看齐。
Node 中的 Event Loop
上文的 timer 算一个小插曲,我们现在回归本文核心——Event Loop。
让我们聚焦在node
的实现上,blink
的实现本文不做展开,主要是因为:
chrome
行为目前看来和规范一致- 可参考的文档不多
- 不会搜索,根本不知道核心代码从何找起。。。
(略过所有研究过程。。。)
直接看结论,下图是node
的Event Loop实现:
补充说明:
Node
的 Event Loop 分阶段,阶段有先后,依次是- expired timers and intervals,即到期的 setTimeout/setInterval
- I/O events,包含文件,网络等等
- immediates,通过 setImmediate 注册的函数
- close handlers,close 事件的回调,比如TCP连接断开
- 同步任务及每个阶段之后都会清空 microtask 队列
- 优先清空next tick queue,即通过
process.nextTick
注册的函数 - 再清空other queue,常见的如Promise
- 而和规范的区别,在于node会清空当前所处阶段的队列,即执行所有task
重新挑战测试(2)
了解了实现,再回头看测试(2)
:
// 代码简略表示
// 1
setTimeout(() => {
// ...
})
// 2
setTimeout(() => {
// ...
})
可以看出由于两个setTimeout
延时相同,被合并入了同一个expired timers queue,而一起执行了。所以,只要将第二个setTimeout
的延时改成超过2ms(1ms无效,详见上文),就可以保证这两个setTimeout
不会同时过期,也能够保证输出结果的一致性。
那如果我把其中一个setTimeout
改为setImmediate
,是否也可以做到保证输出顺序?
答案是不能。虽然可以保证setTimeout
和setImmediate
的回调不会混在一起执行,但无法保证的是setTimeout
和setImmediate
的回调的执行顺序。
在node
下,看一个最简单的例子,下面代码的输出结果是无法保证的:
setTimeout(() => {
console.log(0)
})
setImmediate(() => {
console.log(1)
})
// or
setImmediate(() => {
console.log(0)
})
setTimeout(() => {
console.log(1)
})
问题的关键在于setTimeout
何时到期,只有到期的setTimeout
才能保证在setImmediate
之前执行。
不过如果是这样的例子(2)
,虽然基本能保证输出的一致性,不过强烈不推荐:
// 先使用setTimeout注册
setTimeout(() => {
// ...
})
// 一系列micro tasks执行,保证setTimeout顺利到期
new Promise(resolve => {
// ...
})
process.nextTick(() => {
// ...
})
// 再使用setImmediate注册,“几乎”确保后执行
setImmediate(() => {
// ...
})
或者换种思路来保证顺序:
const fs = require('fs')
fs.readFile('/path/to/file', () => {
setTimeout(() => {
console.log('timeout')
})
setImmediate(() => {
console.log('immediate')
})
})
那,为何这样的代码能保证setImmediate
的回调优先于setTimeout
的回调执行呢?
因为当两个回调同时注册成功后,当前node
的Event Loop正处于I/O queue阶段,而下一个阶段是immediates queue,所以能够保证即使setTimeout
已经到期,也会在setImmediate
的回调之后执行。
PART 3:应用
由于也是刚刚学习Event Loop,无论是依托于规范还是实现,我能想到的应用场景还比较少。那掌握Event Loop,我们能用在哪些地方呢?
查Bug
正常情况下,我们不会碰到非常复杂的队列场景。不过万一碰到了,比如执行顺序无法保证的情况时,我们可以快速定位到问题。
面试
那什么时候会有复杂的队列场景呢?比如面试,保不准会有这种稀奇古怪的测试,这样就能轻松应付了~
执行优先级
说回正经的,如果从规范来看,microtask优先于task执行。那如果有需要优先执行的逻辑,放入microtask队列会比task更早的被执行,这个特性可以被用于在框架中设计任务调度机制。
如果从node
的实现来看,如果时机合适,microtask的执行甚至可以阻塞I/O,是一把双刃剑。
综上,高优先级的代码可以用Promise
/process.nextTick
注册执行。
执行效率
从node
的实现来看,setTimeout
这种timer类型的API,需要创建定时器对象和迭代等操作,任务的处理需要操作小根堆,时间复杂度为O(log(n))。而相对的,process.nextTick
和setImmediate
时间复杂度为O(1),效率更高。
如果对执行效率有要求,优先使用process.nextTick
和setImmediate
。
1.node中使用定时器的问题在于,它并非精确的.譬如setTimeout()设定一个任务在10ms后执行,但是在9ms后,有一个任务占用了5ms,再次轮到定时器时,已经耽误了4ms.
好了node中的定时器就简单的讲这么多.
2.看代码:
process.nextTick(function(){
console.log("延迟执行");
});
console.log("正常执行1");
console.log("正常执行2");
console.log("正常执行3");
console.log("正常执行4");
通过这个例子,我想大家很清楚的就能看到nextTick()是用来干嘛的了.主要是用来异步执行的.
在看代码:
setImmediate(function(){
console.log("延迟执行");
});
console.log("正常执行");
我们发现 setImmediate 也是异步执行的,奇怪了。
那么它与 nextTick() 有什么区别呢?
看代码:
代码一:
process.nextTick(function(){
console.log("nextTick延迟")
});
setImmediate(function(){
console.log("setImmediate延迟");
});
console.log("正常执行");
结果:
代码二:
setImmediate(function(){
console.log("setImmediate延迟");
});
process.nextTick(function(){
console.log("nextTick延迟")
});
console.log("正常执行");
结果:
发现代码虽然顺序不一样,但是执行的结果是一样的.
从结果可以发现:
nextTick() 的回调函数执行的优先级要高于 setImmediate();
process.nextTick() 属于 idle 观察者,setImmediate() 属于 check 观察者,在每一轮循环检查中,idle 观察者先于 I/O 观察者,I/O 观察者先于 check 观察者。
在具体实现上,process.nextTick() 的回调函数保存在一个数组中,setImmediate() 的结果则是保存在链表中,在行为上,process.nextTick()在每轮循环中会将数组中的回调函数全部执行完,而 setImmediate() 在每轮循环中执行链表中的一个回调函数.
// 加入2个 nextTick() 的回调函数
process.nextTick(function(){
console.log("nextTick延迟执行1");
});
process.nextTick(function(){
console.log("nextTick延迟执行2");
});
//加入两个setImmediate()回调函数
setImmediate(function(){
console.log("setImmediate延迟执行1");
process.nextTick(function(){
console.log("强势插入");
});
});
setImmediate(function(){
console.log("setImmediate延迟执行2");
});
console.log("正常执行");
从执行结果上看出:当第一个setImmediate()的回调函数执行完后,并没有立即执行第二个,而是进入了下一轮循环,再次按nextTick()优先,setImmediate()次后的顺序执行.之所以这样设计,是为了保证每次循环能够较快的执行结束.防止CPU占用过多而阻塞后续I/O调用的情况.
区别:
1.在理解两者的区别之前要说一下轮询
前面博客也有记录,nodejs中是事件驱动的,有一个循环线程一直从事件队列中取任务执行或者I/O的操作转给后台线程池来操作,把这个循环线程的每次执行的过程算是一次轮询.
2.setImmediate()的使用
即时计时器立即执行工作,它是在事件轮询之后执行,为了防止轮询阻塞,每次只会调用一个。
3.Process.nextTick()的使用
它和setImmediate()执行的顺序不一样,它是在事件轮询之前执行,为了防止I/O饥饿,所以有一个默认process.maxTickDepth=1000来限制事件队列的每次循环可执行的nextTick()事件的数目。
4.总结
在网上百度的关于它们的总结:
nextTick() 的回调函数执行的优先级要高于 setImmediate();
process.nextTick() 属于idle观察者,setImmediate()属于check观察者,在每一轮循环检查中,dle观察者先于I/O观察者,I/O观察者先于check观察者。
在具体实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中,在行为上,process.nextTick() 在每轮循环中会将数组中的回调函数全部执行完,而setImmediate()在每轮循环中执行链表中的一个回调函数。
//加入2个nextTick()的回调函数
process.nextTick(function(){
console.log("nextTick延迟执行A");
});
process.nextTick(function(){
console.log("nextTick延迟执行B");
setImmediate(function(){
console.log("setImmediate延迟执行C");
});
process.nextTick(function(){
console.log("nextTick延迟执行D");
});
});
//加入两个setImmediate()回调函数
setImmediate(function(){
console.log("setImmediate延迟执行E");
process.nextTick(function(){
console.log("强势插入F");
});
setImmediate(function(){
console.log("setImmediate延迟执行G");
});
});
setImmediate(function(){
console.log("setImmediate延迟执行H");
process.nextTick(function(){
console.log("强势插入I");
});
process.nextTick(function(){
console.log("强势插入J");
});
setImmediate(function(){
console.log("setImmediate延迟执行K");
});
});
console.log("正常执行L");
正常执行L
nextTick延迟执行A
nextTick延迟执行B
nextTick延迟执行D
setImmediate延迟执行E
setImmediate延迟执行H
setImmediate延迟执行C
强势插入F
强势插入I
强势插入J
setImmediate延迟执行G
setImmediate延迟执行K
Promise then 中回调为什么是异步执行?
Promise 的机制就是 then 回调函数必须异步执行。为什么?因为这样保障了代码执行顺序的一致性。
先看一个场景:
promise.then(function(){
if (trueOrFalse) {
// 同步执行
foo();
} else {
// 异步执行 (如:使用第三方库)
setTimeout(function(){
foo();
})
}
});
bar();
- 如果 promise then 回调是同步执行的,请问 foo() 和 bar() 函数谁先执行?
答案是,如果 trueOrFalse 为 true 则 foo() 先执行,bar() 后执行;否则 bar() 先执行,foo() 后执行。在大部分情况下,你没法预料到 trueOrFalse 的值,这也就意味着,你不能确定这段代码真正的执行顺序,这可能会导致一些难以想到的 bug。 - 如果 promise then 回调是异步执行的,请问 foo() 和 bar() 函数谁先执行?
答案一目了然,bar() 先执行,foo() 后执行。
所以为了保证代码执行顺序的一致性, then 回调必须保证是异步的。
https://www.talkingcoder.com/article/6420924177101619200
setTimeout(0)单线程和异步队列
setTimeout和setInterval是JS内置的两个定时器,使用很简单,但这两个方法背后的原理却不简单。
我们知道,JS是单线程语言,在浏览器中,当JS代码被加载时,浏览器会为其分配一个主线程来执行任务(函数),主线程会形成一个全局执行环境,执行环境采用栈的方式将待执行任务按顺序依次来执行。
但在浏览器中有一些任务是非常耗时的,比如http请求、定时器、事件回调等,为了保证其他任务的执行效率不被影响,JS在执行环境中维护了一个异步队列(也叫工作线程),并将这些任务放入队列中进行等待,这些任务的执行时机并不确定,只有当主线程的任务执行完成以后,才会去检查异步队列中的任务是否需要开始执行。这就是为什么setTimeout(fn,0) 始终要等到最后执行的原因。关于单线程和异步队列问题请参考:setTimeout(0)
https://www.talkingcoder.com/article/6394348388350903833
常青:JavaScript中的JS引擎的执行机制:探究Event Loopzhuanlan.zhihu.com
JavaScript中的JS引擎的执行机制:探究Event Loop
常青:JavaScript中的JS引擎的执行机制:探究Event Loopzhuanlan.zhihu.com
一、JavaScript是单线程
javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。、
二、任务队列
2.1 同步任务与异步任务
单线程就意味着,所有任务需要排队。所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。异步执行的运行机制如下:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
2.2 js 引擎模型
从宏观角度讲, js 的执行是单线程的. 所有的异步结果都是通过 “任务队列(Task Queue)” 来调度被调度. 消息队列中存放的是一个个的任务(Task). 规范中规定, Task 分为两大类, 分别是 Macro Task 和 Micro Task, 并且每个 Macro Task 结束后, 都要清空所有的 Micro Task. 宏观上讲, Macrotask 会进入 Macro Task Queue, Microtask 会进入 Micro Task Queue。而 Micro Task 被分到了两个队列中. ‘Micro Task Queue’ 存放 Promise
等 microtask. 而 ‘Tick Task Queue’ 专门用于存放 process.nextTick
的任务.现在先来看看规范怎么做的分类.
Macrotask 包括:
setImmediate
setTimeout
setInterval
Microtask 包括:
process.nextTick
Promise
Object.observe
MutaionObserver
所说的, ‘每个 Macro Task 结束后, 都要清空所有的 Micro Task‘. 引擎会遍历 Macro Task Queue, 对于每个 Macrotask 执行完毕后都要遍历执行 Tick Task Queue 的所有任务, 紧接着再遍历 Micro Task Queue 的所有任务. (nextTick
会优于Promise
执行)
三、Event Loop
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
三种任务队列中的代码执行流程图如下:
来个例子检验一下吧
console.log('main1');
process.nextTick(function() {
console.log('process.nextTick1');
});
setTimeout(function() {
console.log('setTimeout');
process.nextTick(function() {
console.log('process.nextTick2');
});
}, 0);
new Promise(function(resolve, reject) {
console.log('promise');
resolve();
}).then(function() {
console.log('promise then');
});
console.log('main2');
输出结果为
main1
promise
main2
process.nextTick1
promise then
setTimeout
process.nextTick2
分析如下
- 开始执行代码,输出 main1,process.nextTick 放入tickTaskQueen,setTimeout放入 macroTaskQueen, new Promise 执行 输出 promise,then 方法 放入 MicroTaskQueen , 接着 最后一行代码 console.log 输出 main2
- 当前的 宏任务执行完毕,开始清空微任务,先清空tickTaskQueen ,执行 console.log('process.nextTick1'); 输出'process.nextTick1;再清空MicroTaskQueen执行 console.log('promise then'); 输出promise then;微任务全部清空。
- 开始下次 eventLoop; 执行 setTimeout; 第一行 console.log('setTimeout'); 输出setTimeout; process.nextTick 将任务放入了tickTaskQueen;当前宏任务执行完毕;开始清空MicroTaskQueen,清空tickTaskQueen ,执行 console.log('process.nextTick2');输出process.nextTick2;
四、Node.js 的 Event Loop
Node.js 也是单线程的 Event Loop,但是它的运行机制不同于浏览器环境。
(1)V8 引擎解析 JavaScript 脚本。
(2)解析后的代码,调用 Node API。
(3)libuv库 负责 Node API 的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
(4)V8引擎再将结果返回给用户。
Node.js 还提供了另外两个与 任务队列 有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对"任务队列"的理解。
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
上面代码中,由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。
setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});
// 1
// TIMEOUT FIRED
// 2
上面代码中,setImmediate和setTimeout被封装在一个setImmediate里面,它的运行结果总是1--TIMEOUT FIRED--2,这时函数A一定在timeout前面触发。至于2排在TIMEOUT FIRED的后面(即函数B在timeout后面触发),是因为setImmediate总是将事件注册到下一轮Event Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮 Loop 执行。
我们由此得到了 process.nextTick 和 setImmediate 的一个重要区别:多个 process.nextTick 语句总是在当前 执行栈 一次执行完,多个 setImmediate 可能则需要多次 loop 才能执行完。
参考文档
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论