首先 setTimeout 并没有特殊,也是一个 task。另外每次的执行过 task 和 大量的 microtask(不一定在一次循环全执行完)后,会进行 renderUi 阶段,虽然不是每次事件循环都进行 renderUi ,但每次间隔,也就是传说中 60hz 的一帧 16ms。
浏览器中的事件循环
宏任务和微任务
同步代码的执行也属于宏任务
浏览器先执行同步代码,遇到宏任务就放到 macro task 的任务队列中,遇到微任务就放入 micro task 的队列中。在执行完同步任务之后就会先执行微任务队列中的所有微任务,然后从宏任务队列中取出第一个宏任务执行,执行完毕之后再去执行所有的微任务...这个过程是循环不断的所以被称为事件循环。但是需要注意的是浏览器会在每 16ms 进行一次的 UI 渲染,可能会中断事件循环的执行
常见的宏任务
setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作、UI 渲染
常见的微任务
MutationObserver Promise
nodejs 中的事件循环
外部输入数据–>轮询阶段(poll)–>检查阶段(check) setImmediate–>关闭事件回调阶段(close callback)–>定时器检测阶段(timer) setTimeout/setInterval–>I/O 事件回调阶段(I/O callbacks) 文件操作的回调函数等等–>闲置阶段(idle, prepare)–>轮询阶段(按照该顺序反复运行)
我们可以接触到的比较常用的就是 poll,check,timer 这几个阶段
nodejs中时间循环和浏览器中的区别
在 node11 版本之后,对于 macro task 和 micro task 的执行时机和浏览器中相同。
在 node11 之前,nodejs 和浏览器中的事件循环的区别就是 micro task 的执行时机;nodejs中 timers 阶段有几个 setTimeout/setInterval 都会依次执行,执行完毕所有的 timers 代码之后才会去执行 micro task,并不像浏览器端,每执行一个宏任务后就去执行所有微任务。
所以下面的代码,在 node11 之前的执行结果是和浏览器中不相同的
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
// 浏览器和新版本node start=>end=>promise3=>timer1=>promise1=>timer2=>promise2
poll阶段 该阶段会执行所有的回调
poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情
回到 timer 阶段执行回调
执行 I/O 回调
没有设置timer或者是设置的timer没有到时间
会发生以下两件事情
如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
如果 poll 队列为空时,会有两件事发生
如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调
如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去
设置了timer且timer时间已经到了
当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。
注意点
(1) setTimeout 和 setImmediate
二者非常相似,区别主要在于调用时机不同。
setImmediate 设计在 poll 阶段完成时执行,即 check 阶段;
setTimeout 设计在 poll 阶段为空闲时,且设定时间到达后执行,但它在 timer 阶段执行
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
对于以上代码来说,setTimeout 可能执行在前,也可能执行在后。
首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的
我们发现虽然我们传的超时时间是 0,但是 0 不是合法值,nodejs 会把超时时间变成 1。
进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调
如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了
但当二者在异步 i/o callback 内部调用时,总是先执行 setImmediate,再执行 setTimeout
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
// immediate
// timeout
在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。
(2) process.nextTick
这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
})
})
})
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1