什么是事件循环
在了解事件循环前,需要一些有关JS特性的前置知识。
JS引擎是单线程的,直白来说就是一个时间点下JS引擎只能去做一件事情,而Java这种多线程语言,可以同时做几件事情。
JS做的任务分为同步和异步两种,所谓 “异步”,简单说就是一个任务不是连续完成的,先执行第一段,等做好了准备,再回过头执行第二段,第二段也被叫做回调;同步则是连贯完成的。
像读取文件、网络请求这种任务属于异步任务:花费时间很长,但中间的操作不需要JS引擎自己完成,它只用等别人准备好了,把数据给他,他再继续执行回调部分。
如果没有特殊处理,JS引擎在执行异步任务时,应该是存在等待的,不去做任何其他事情。用一个图来展示这个过程,可以看出,在执行异步任务时有大量的空闲时间被浪费。
实际上这是大多数多线程语言的处理办法。但对于JS这种单线程语言来说,这种长时间的空闲等待是不可接受的:遇到其他紧急任务,Java可以再开一个线程去处理,JS却只能忙等。
所以采取了以下的“异步任务回调通知”模式:
在等待异步任务准备的同时,JS引擎去执行其他同步任务,等到异步任务准备好了,再去执行回调。这种模式的优势显而易见,完成相同的任务,花费的时间大大减少,这种方式也被叫做非阻塞式。
而实现这个“通知”的,正是事件循环,把异步任务的回调部分交给事件循环,等时机合适交还给JS线程执行。事件循环并不是JavaScript首创的,它是计算机的一种运行机制。
事件循环是由一个队列组成的,异步任务的回调遵循先进先出,在JS引擎空闲时会一轮一轮地被取出,所以被叫做循环。
根据队列中任务的不同,分为宏任务和微任务。
宏任务和微任务
事件循环由宏任务和在执行宏任务期间产生的所有微任务组成。完成当下的宏任务后,会立刻执行所有在此期间入队的微任务。
这种设计是为了给紧急任务一个插队的机会,否则新入队的任务永远被放在队尾。区分了微任务和宏任务后,本轮循环中的微任务实际上就是在插队,这样微任务中所做的状态修改,在下一轮事件循环中也能得到同步。
常见的宏任务有:script(整体代码)/setTimout/setInterval/setImmediate(node 独有)/requestAnimationFrame(浏览器独有)/IO/UI render(浏览器独有)
常见的微任务有:process.nextTick(node 独有)/Promise.then()/Object.observe/MutationObserver
宏任务setTimeout的误区
setTimeout的回调不一定在指定时间后能执行。而是在指定时间后,将回调函数放入事件循环的队列中。
如果时间到了,JS引擎还在执行同步任务,这个回调函数需要等待;如果当前事件循环的队列里还有其他回调,需要等其他回调执行完。
另外,setTimeout 0ms 也不是立刻执行,它有一个默认最小时间,为4ms。
所以下面这段代码的输出结果不一定:
// node
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
因为取出第一个宏任务之前在执行全局Script,如果这个时间大于 4ms,这时 setTimeout 的回调函数已经放入队列,就先执行 setTimeout;如果准备时间小于 4ms,就会先执行 setImmediate。
浏览器的事件循环
浏览器的事件循环由一个宏任务队列+多个微任务队列组成。
首先,执行第一个宏任务:全局Script脚本。产生的的宏任务