SetTimeout、SetInterVal、setImmediate和process.nextTick的理解

起航

以前都是在掘金上看别人的文章,好的点个赞。逛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。

转载于:https://juejin.im/post/5aa0a1475188255589495c20

你可能感兴趣的:(SetTimeout、SetInterVal、setImmediate和process.nextTick的理解)