起航
以前都是在掘金上看别人的文章,好的点个赞。逛github的时候,也只是喜欢看别人源码,也从不在git上上传东西。今天突然萌发了自己想要发一些东西,方便自己以后查阅,也能让大家指点一下,是否正确。免得在错误的道路上越走越远,还和正确的人争论的面红耳赤。毕竟一旦确立了错误的观点,那么错误的观点就会在你潜意识里变成正确的了。
疑问
正常情况下,setTimeout、setInterval、setImmediate和process.nextTick都是异步执行的,那么这四个函数方法的执行机制和时间到底是如何的呢,各自有什么区别呢?能否替换呢?这一切都要从nodejs的event loop上面出发才能 有所理解吧。
setTimeout和setInterval
先分别介绍各个函数,setTimeout和setInterval最为相似,在函数分析上,我们知道,setTimeout和setInterval的函数格式都是如下:
setTimeout(function(arg1,arg2){
//some code
},XXX)
setInterval(function(arg1,arg2){
//some code
},XXX)
复制代码
那么这个XXX延迟时间是有个规定的,延迟时间的范围是[1,2^31-1]。当你延迟时间设定小于1或者大于2^31-1的时候,延迟时间默认被修改成1,即当你写setTimeout(function(arg1,arg2){},0.1)其实等价于写了setTimeout(function(arg1,arg2){},1)。
setImmediate和nextTick
我们直接看代码,这两个函数的执行结果如何:
setImmediate(function(){
console.log('immediate')
})
process.nextTick(function(){
console.log('next tick')
})
复制代码
交换代码顺序
process.nextTick(function(){
console.log('next tick')
})
setImmediate(function(){
console.log('immediate')
})
复制代码
我们发现代码输出的结果是一样的。那么nextTick的执行机制实在setImmediate之前的。
4个函数的执行机制
在介绍4个函数的机制之前,我们来看一个有趣的现象
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
复制代码
输出的结果时而是
setTimeout
setImmediate
复制代码
时而是
setImmediate
setTimeout
复制代码
为啥这两个函数的执行的顺序如此不固定呢?难道有随机性存在吗? 其实不然,我们来看一张图:
这是整个event loop的简略图,很多东西我都删减掉了,I/O里面的细节操作我逗缩写在一个步骤里了。 我们用通俗距离方法来说吧,setTimeout和setInterval的等级是一样的,所以方法在代码里按照先后顺序注册执行。但是按上面代码输出,为什么1和3的步骤会出现随机性输出呢?setTimeout的回调函数在1阶段执行,setImmediate的回调函数在3阶段执行。event loop先检测1阶段,这个是正确的,官方文档也说了The event loop cycle is timers -> I/O -> immediates, rinse and repeat. 但是有个问题就是进入第一个event loop时间不确定,不一定就是从头开始进 入的,上面的例子进入的时间并不完整。网上有人总结,当进入event loop的 时间低于1ms,则进入check阶段,也就是3阶段,调用setImmediate,如果超过1ms,则进入的是timer阶段,也就是1阶段,回调setTimeout的回调函数。
所以4个函数的机制我们可以总结了:在1阶段(timer阶段),我们注册的是setTimeout和setInterval回调函数,在I/O阶段之后的3阶段(check阶段),我们注册的是setImmediate的回调函数。现在就剩下process.nextTick函数了。这个函数比较特殊,他注册时间实在上图中绿色箭头的tick阶段。
用些题目来加深下理解(这些题目都是从网上copy而来)
题目一
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
})
复制代码
运行结果:
setImmediate
setTimeout
复制代码
理由:
timer -- I/O -- check。这三个阶段是event loop的执行顺序,当fs读取文件时,我们已经将setTimeout和setImmediate注册在event loop中了,当fs文件流读取完毕,执行到了I/O阶段,然后去执行check阶段,执行setImmediate的回调函数,然后去下一次轮询的时候进入到timer阶段执行setTimeout。
题目二
setInterval(() => {
console.log('setInterval')
}, 100)
process.nextTick(function tick () {
process.nextTick(tick)
})
复制代码
运行结果:
无任何输出,setInterval永远不执行
复制代码
理由:
因为process.nextTick是注册在tick阶段的,回调的仍然是process.nextTick方法,但是process.nextTick不是注册在下一个轮询的tick阶段,而是在当前的tick阶段进行拼接,继续执行,从而导致了死循环,event loop根本没机会进入到timer阶段
###题目三
setImmediate(() => { ------ 1
console.log('setImmediate1')
setImmediate(() => { ------2
console.log('setImmediate2')
})
process.nextTick(() => { -------3
console.log('nextTick')
})
})
setImmediate(() => { ------4
console.log('setImmediate3')
})
复制代码
运行结果:
setImmediate1
setImmediate3
nextTick
setImmediate2
复制代码
理由:
先将最外层第一个setImmediate,即标号为1注册,然后注册最外层标号为2的setImmediate。接下来注册第一个setImmediate里面的异步函数。先注册标号为3的setImmediate的函数,然后注册标号为4的process.nextTick。此时进入event loop执行回调,先执行1里面的函数,输出setImmediate1,由于3和4都在2之后注册的,此时执行的是标号为4的回调方法,输出setImmediate3。继续轮询,由于process.nextTick是注册在4之后的tick中,所以先执行process.nextTick,最好轮询执行2的回调方法,输出setImmediate2
题目四
const promise = Promise.resolve()
promise.then(() => {
console.log('promise')
})
process.nextTick(() => {
console.log('nextTick')
})
复制代码
输出结果:
nextTick
promise
复制代码
理由:
promise.then也是注册在tick阶段的,但是process.nextTick的优先级高于promise,故而先调用process.nextTick
题目五
setTimeout(() => {
console.log(1)
}, 0)
new Promise((resolve, reject) => {
console.log(2)
for (let i = 0; i < 10000; i++) {
i === 9999 && resolve()
}
console.log(3)
}).then(() => {
console.log(4)
})
console.log(5)
复制代码
输出结果:
2
3
5
4
1
复制代码
理由:
new promise是个同步操作,故而输出2和3,然后执行最后一行代码输出5。接下来就是promise.then和setTimeout的问题了。我们知道promise.then和process.nextTick一样是注册在tick阶段的,而setTimeout是注册在timer阶段的,先进入tick阶段执行,然后在进入到下一个轮询的setTimeout。
题目六
setImmediate(() => {
console.log(1)
setTimeout(() => {
console.log(2)
}, 100)
setImmediate(() => {
console.log(3)
})
process.nextTick(() => {
console.log(4)
})
})
setImmediate(() => {
console.log(5)
setTimeout(() => {
console.log(6)
}, 100)
setImmediate(() => {
console.log(7)
})
process.nextTick(() => {
console.log(8)
})
})
复制代码
输出结果
1
5
4
8
3
7
2
6
复制代码
理由:
这里的tick会合并,所以4和8连续输出
题目七
setImmediate(() => { ---1
console.log(1)
setTimeout(() => { ---2
console.log(2)
}, 100)
setImmediate(() => { ---3
console.log(3)
})
process.nextTick(() => { ---4
console.log(4)
})
})
process.nextTick(() => { ---5
console.log(5)
setTimeout(() => { ---6
console.log(6)
}, 100)
setImmediate(() => { ---7
console.log(7)
})
process.nextTick(() => { ---8
console.log(8)
})
})
console.log(9)
复制代码
输出结果
9
5
8
1
7
4
3
6
2
复制代码
理由: 如图所示
补充: 1.macrotask:script中代码、setTimeout、setInterval、I/O、UI render。
2.microtask: promise、Object.observe、MutationObserver,process.nextTick。