Event Loop
概念
JavaScript是一门单线程语言,同一时间内只能处理一件事情。
JavaScript 事件循环
既然js是单线程,处理任务是顺序执行的,那如果要处理的任务多了,就需要排队了,万一某一个任务一直没有处理完,那后面的任务就会一直堵塞,故任务需要区分为同步任务和异步任务

图中表示的意思是:
js在执行任务的时候,将区分同步任务和异步任务,同步任务进入主线程执行,异步任务进入Event Table,并注册回调函数- 当指定的事情完成时,
Event Table会将这个回调函数移入Event Queue - 主线程内的任务执行完毕为空时,会去
Event Queue读取对应的回调函数进入主线程执行 - 然后不断重复上述过程,也就是常说的
Event Loop(事件循环)
注:js引擎存在monitoring process进程,会持续不断检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数
简单示例
var data = []
$.ajax({
url: 'www.js.com',
data: data,
success: function() {
console.log('请求成功')
}
})
console.log('结束')以上是一段简易的ajax请求代码,看看js是如何执行的
ajax进入Event Table,并注册回调函数success- 执行
console.log('结束') ajax事件完成,回调函数success进入Event Queue- 主线程从
Event Queue中读取回调函数success并执行
原来ajax请求干了这么多事情0.0...
setTimeout
常用于模拟耗时操作的setTimeout,有时也并不是很精准,写的延时1000ms执行,但是有时候却可能有些差别,先看以下实例
var start = Date.now()
setTimeout(function() {
var end = Date.now()
console.log('延时1秒后执行', end - start)
}, 1000)
console.log('我先执行')我们都知道,以上代码打印顺序如下
// 我先执行
// '延时1秒后执行' 1000看起来貌似没什么毛病,但是运行多几次就会发现,1000并不一直是1000,还可能会是1001、1002...
假如主线程中存在非常耗时的操作,例如以下伪代码:
setTimeout(function() {
task()
}, 1000)
sleep(10000) // 假如这是一个非常耗时的函数此时,setTimeout的延时1000ms执行,就差别很多了,先看看是怎么执行的
js执行过程中遇到setTimeout,首先将task()放入Event Table并注册,计时开始- 往下执行遇到
sleep函数,但是sleep函数执行得很慢,计时还在继续 - 1秒到了,计时事件
setTimeout完成,task()进入Event Queue,但是sleep函数还在占用主线程,还没执行完,只能继续等待 sleep函数终于执行完毕,task()终于从Event Queue进入主线程执行
现在我们知道此时的setTimeout的含义是:
在1秒之后,需要把task()加入到Event Queue中,
等待主线程空闲时进入主线程执行,
但是当主线程正在处理耗时操作时,那就只能等着,
从而导致了延迟的时间远超过了1秒那我们经常遇到的setTimeout(fn, 0),是不是就会立即执行呢?
答案是不会的,fn依然会进入Event Queue中等待主线程空闲之后才能进入主线程执行如果主线程为空时,会达到0秒后执行吗?
实际上也不会,根据HTML的标准,最低是4ms,可运行尝试
var start = Date.now()
setTimeout(function() {
var end = Date.now()
console.log('延时0秒?', end - start)
}, 0)setInterval
与setTimeout相似,setInterval是每隔指定时间会将回调函数放入EventQueue中,如果主线程在忙,也同样需要等待。
值得注意的是,放入Event Queue的时间间隔是指定的,但是如果主线程耗时操作大于或等于这个时间间隔,那就完全看不出有时间间隔执行了,伪代码示例如下:
setInterval(fn, 1000) // 每间隔1000ms将回调fn置入Event Queue
sleep(1000) // 假如这个函数将耗时1000ms,以上的间隔1000ms执行则看不出是有间隔的执行了Promise 与 process.nextTick(callback)
Promise的定义和功能不再赘述,不了解的读者可以学习一下阮一峰老师的Promise
而process.nextTick(callback)类似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。
除了同步任务和异步任务,我们需要对任务有更精细的定义:
macro-task(宏任务):包含整体代码script、setTimeout、setIntervalmicro-task(微任务):Promise、process.nextTick
宏任务和微任务会进入不同的Event Queue,js在执行过程中,进入整体代码(宏任务),开始第一次循环,接着执行所有微任务,然后再次从宏任务开始执行任务队列,完毕之后再次执行所有微任务,依次循环
先看以下代码:
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(function(resolve) {
console.log('promise')
resolve()
}).then(function() {
console.log('then')
})
console.log('console')- 整体代码作为宏任务,进入主线程
- 先遇到
setTimeout,将其回调函数注册后分发到宏任务Event Queue - 然后遇到
new Promise,立即执行其中的函数打印出promise,then函数则分发到微任务Event Queue - 往下遇到
console.log(),直接打印出console - 此时整体代码作为第一个宏任务执行结束,首先看看有哪些微任务?此时微任务
Event Queue中发现了then,取出执行,打印出then - 第一轮事件循环结束,开始第二轮循环,从宏任务
Event Queue开始,此时发现宏任务Event Queue中有setTimeout对应的回调函数,立即执行 - 结束
事件循环、宏任务、微任务关系图如下:

经典示例
检验是否掌握了js执行机制,看以下代码
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})详细分析
第一轮
整体代码作为宏任务进入主线程执行
首先遇到
console.log('1'),直接打印,此时控制台输出 1
宏任务Event Queue | 微任务Event Queue | 控制台console |
|---|---|---|
| - | - | 1 |
- 往下遇到
setTimeout,没有设置延迟时间,则默认为0,将其回调函数注册到Event Table后立即分发至宏任务Event Queue,至于回调函数中有什么,先不管,因为要等待主线程空闲了才能进入主线程去执行,此时的任务队列里面是这样的
宏任务Event Queue | 微任务Event Queue | 控制台console |
|---|---|---|
1. setTimeout | - | 1 |
- 再往下遇到
process.nextTick,将其注册后分发至微任务Event Queue,此时
宏任务Event Queue | 微任务Event Queue | 控制台console |
|---|---|---|
1. setTimeout | 1. process.nextTick | 1 |
- 然后往下遇到
new Promise,直接执行里面代码打印,此时控制台输出 7,再调用resolve(),将then分发至微任务Event Queue中。此时微任务Event Queue中包含:process.nextTick和then的回调
宏任务Event Queue | 微任务Event Queue | 控制台console |
|---|---|---|
1. setTimeout | 1. process.nextTick | 1 7 |
| - | 2. then |
- 最后遇到
setTimeout,同样没有设置延迟时间,则默认为0,将其回调立即分发至宏任务Event Queue,里面有什么,先不管,还没到他执行,注意区别于前一个setTimeout
宏任务Event Queue | 微任务Event Queue | 控制台console |
|---|---|---|
1. setTimeout | 1. process.nextTick | 1 7 |
2. setTimeout | 2. then |
至此,整体代码作为宏任务执行完毕,主线程此时空闲了,就会去看看微任务
Event Queue中是否有任务,如果有,则将其执行清空,此时发现微任务Event Queue中包含两个任务,则依次将process.nextTick与then添加至主线程执行:先执行
process.nextTick的回调,此时控制台输出 6,然后执行then,此时此时控制台输出 8
宏任务Event Queue | 微任务Event Queue | 控制台console |
|---|---|---|
1. setTimeout | 1 7 6 8 | |
2. setTimeout |
- 清空完微任务
Event Queue后,至此,一轮事件循环已结束,主线程立即进入下一轮事件循环,下一轮的事件循环,依旧采用这样的思路进行分析
第二轮
主线程将宏任务
Evenet Queue中的第一个setTimeout回调取出执行,首先遇到console.log('2'),直接打印,此时控制台输出 2往下遇到
process.nextTick,将其分发至微任务Event Queue等待往下遇到
new Promise,直接执行其中代码,先打印 4,此时控制台输出 4,然后执行resolve(),将then分发至微任务Event Queue至此,第二轮事件循环中的宏任务执行完毕,现在看看任务队列中都有些什么
宏任务Event Queue | 微任务Event Queue | 控制台console |
|---|---|---|
2. setTimeout | 1. process.nextTick | 1 7 6 8 2 4 |
2. then |
此时主线程发现微任务
Event Queue中又有两个任务可执行,然后又依次调用执行并将其清空,首先执行process.nextTick,打印出 3,此时控制台输出 3然后执行
then,打印出 5,此时控制台输出 5微任务
Event Queue执行完毕,第二轮事件循环正式结束,此时任务队列如下
宏任务Event Queue | 微任务Event Queue | 控制台console |
|---|---|---|
2. setTimeout | 1 7 6 8 2 4 3 5 | |
第三轮
主线程再次将宏任务
Event Queue中的第二个setTimeout取出执行,首先就遇到了打印 9,此时控制台输出 9往下执行遇到
process.nextTick,分发至微任务Event Queue中等待再往下遇到
new Promise,执行其中代码,打印出 11,此时控制台输出 11,执行resolve(),将then分发至微任务Event Queue,宏任务执行完毕,此时的任务队列如下
宏任务Event Queue | 微任务Event Queue | 控制台console |
|---|---|---|
1. process.nextTick | 1 7 6 8 2 4 3 5 9 11 | |
2. then |
此时主线程发现微任务
Event Queue中又有任务,于是有依次去取其中的任务到主线程上执行,首先执行process.nextTick,打印出 10,此时控制台输出 10,然后执行then,打印出 12,此时控制台输出 12至此第三轮事件循环也已结束,此时的宏任务Event Queue与微任务Event Queue中都已清空,此时控制台已打印出全部
宏任务Event Queue | 微任务Event Queue | 控制台console |
|---|---|---|
| 1 7 6 8 2 4 3 5 9 11 10 12 | ||
整段代码,进行了三次事件循环,才执行完毕,打印输出的顺序跟代码顺序毫无相干,可见:理解其中的事件循环机制多么的重要。
(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)
Finally
- javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的
- 事件循环
Event Loop是js实现异步的一种方法,也是js的执行机制 - 微任务和宏任务还有很多种类,比如
setImmediate等等,执行都是有共同点的