众所周知,JavaScript
这门语言是单线程的。那也就是说,JavaScript
在同一时间只能作一件事,后面的事情必须等前面的事情作完以后才能获得执行。javascript
JavaScript
单线程这件事乍一看好像没毛病,代码原本就是须要按顺序执行的嘛,先来后到,后面的你就先等着。若是是计算量致使的排队,那没办法,老老实实排吧。但若是是由于 I/O
很慢(好比发一个 Ajax
请求,须要 200ms 才能返回结果),那这个等待时间就没太必要了,彻底能够先执行后面其余的任务,等你请求的数据回来了再执行 Ajax
后面的操做嘛。html
由此,
JavaScript
中的任务分红了两种,第一种是同步任务,指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;第二种是异步任务,指的是不进入主线程、而进入“任务队列”的任务,只有“任务队列”通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行。java
其执行过程以下:node
JavaScript
引擎运行JavaScript
时,有一个主线程和一个任务队列。- 同步任务跑在主线程上面,异步任务扔进任务队列中进行等待。
- 主线程中的任务执行完毕以后,回去看看任务队列中有没有异步任务到了须要触发的时机。若是有,那就开始执行异步任务。
- 重复的执行主线程的任务和轮询任务队列。
这种主线程不断地从任务队列中读取任务的机制称为 Event Loop
(事件循环)。promise
在讲 Event Loop
以前,咱们先来了解一下 macrotask
(宏任务)和 microtask
(微任务)。浏览器
包括 setTimeout
、setInterval
、setImmediate
(浏览器仅 IE10 支持)、I/O
、UI Rendering
。bash
包括 process.nextTick
(node
独有)、Promise
、Object.observe
(已废弃)、MutatinObserver
。异步
这里多说一句,Promise
的执行函数(也就是 new Promise(fn)
中的 fn
)是同步任务。socket
Event Loop
的实如今浏览器和 node
中是不同的,咱们先看浏览器。async
- 开始执行主线程的任务
- 主线程的任务执行完毕以后去检查
microtask
队列,将已经到了触发时机的任务放进主线程。- 主线程开始执行任务
- 主线程的任务执行完毕以后去检查
macrotask
队列,将已经到了触发时机的任务放进主线程。- 主线程开始执行任务
- 轮询
microtask
和microtask
好,讲完了流程,来看下🌰。
console.log('script start'); // 同步任务
setTimeout(function() {
console.log('setTimeout'); // 放入 宏任务 队列
}, 0);
new Promise((resolve, reject) => {
console.log('promise'); // 同步任务
resolve();
})
.then(function() {
console.log('promise1'); // 放进 微任务 队列
})
.then(function() {
console.log('promise2'); // 放进 微任务 队列
});
console.log('script end'); // 同步任务
复制代码
根据上面的标识,先执行同步任务,打印出 “script start” 、 “promise” 、 “script end”,而后开始检查 microtask
队列,打印出 “promise1” 和 “promise2”,而后去检查 macrotask
队列,打印出 “setTimeout”。
这里 setTimeout
虽然它的延迟时间为 0,但它是个宏任务,因此必须等同步任务和微任务执行完毕以后才轮到它。
在看一个🌰。
console.log('script start'); // 同步任务
async function async1() {
await async2();
console.log('async1 end'); // 这里就是 then 里面的代码,放入 微任务 队列
}
async function async2() {
console.log('async2 end'); // 同步任务
}
async1();
setTimeout(function() {
console.log('setTimeout'); // 放入 宏任务 队列
}, 0);
new Promise((resolve) => {
console.log('Promise'); // 同步任务
setTimeout(() => {
console.log('setTimeout promise'); // 放入 宏任务 队列
resolve();
});
})
.then(function() {
console.log('promise1'); // 放入 微任务 队列
})
.then(function() {
console.log('promise2'); // 放入 微任务 队列
});
console.log('script end'); // 同步任务
复制代码
这里的 async
和 await
就是 Promise
的语法糖,要懂得转换,其实上述 async
和 await
代码等价于:
new Promise((resolve) => {
new Promise((resolve) => {
console.log('async2 end');
resolve();
});
resolve();
}).then(() => {
console.log('async1 end');
});
复制代码
因此执行顺序为
script start
async2 end
promise
script end
async1 end
setTimeout
setTimeout promise
promise1
promise2
复制代码
有人可能会有疑惑了,打印 “promise1” 和 “promise2” 是微任务,怎么还晚于 setTimeout
宏任务呢?
虽然它们是微任务,可是因为触发它们的 resolve()
处于 setTimeout
宏任务之中,因此它们实际上是在第二轮微任务的轮询中被触发的。
好了,浏览器的 Event Loop
就说到这个,接下来说一下 node
的 Event Loop
。
node
中的 Event Loop
就比较复杂了,英语好的能够去看官方文档。
引用官文档中的一张图,了解一下 Event Loop
的六个阶段。
每一个阶段都有本身的任务队列。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
复制代码
timer:执行setTimeout
和setInterval
中到期的 callback。
pending callback:上一轮循环中少数的 callback 会放在这一阶段执行。
idle, prepare:仅在内部使用。
poll:最重要的阶段,执行 pending callback,在适当的状况下回阻塞在这个阶段。
check:执行setImmediate
(setImmediate()
是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成以后当即执行setImmediate
指定的回调函数)的 callback。
close callbacks:执行 close 事件的 callback,例如socket.on('close'[,fn])
或者http.server.on('close, fn)
。
咱们重点关注 timer
、poll
、check
这三个阶段。
这个阶段执行该阶段任务队列中 setTimeout
和 setInterval
到期的回调,这二者须要设置一个时间。按规则来讲,是到了设定的时间以后就应该执行回调,但在实际状况中,回调函数并非一到设定的时间就能获得执行的,有可能被其余的任务阻塞了,须要等其余任务执行完成以后回调才能获得执行。
好比你设定一个 100ms 以后的 setTimeout
A 回调,可是在 95ms 时执行了一个其余的 B 任务,须要耗时 10ms,那么在时间来到 100ms 的时候,B 任务还在执行当中,那么此时并不会当即执行 A 回调,而是会再等 5ms,等 B 回调完成以后,而后系统发现 A 回调的触发时机已经到了,那赶忙去执行 A 回调。也就是说在这种状况下,A 回调会在 105ms 的时间被执行。
poll 阶段主要有两个事情要作:
I/O
回调。当事件循环到达 poll
阶段时,会有下面两种状况:
poll
队列不为空,那就开始执行队列中的任务,直到队列为空或者达到系统限制。poll
队列为空,那么这种状况又分两种状况;
check
阶段有 setImmediate
任务须要执行,那么就当即结束当前阶段,转到 check
阶段执行该阶段队列中的回调。check
阶段没有 setImmediate
任务须要执行,那么此时会停留在 poll
阶段进行等待,等待有任务进到任务队列中进行执行。在 2.2 的状况中,还会去检查 timer
阶段有没有任务到了执行时间,若是有,那么转入 timer
阶段执行队列中到期的任务。
此阶段会执行 setImmediate
回调,一旦此阶段的任务队列中有了 setImmediate
回调任务,且 poll
阶段的任务执行完了,处于空闲状态,那么就会当即转到 check
阶段执行此阶段任务队列中的任务。
转入此阶段的条件:check
任务队列中有了任务,poll
阶段处于闲置状态,或者 poll
阶段等待超时。
这二者很类似,也有些不一样。
setImmediate
设计用于在当前poll
阶段完成后 check
阶段执行脚本 。setTimeout
安排在通过最小设定时间后运行的脚本,在timers
阶段执行。大部分时间 setImmediate
会比 setTimeout
先执行,但也有例外。好比下列代码:
setImmediate(() => {
console.log('setImmediate');
});
setTimeout(() => {
console.log('setTimeout');
});
复制代码
若是这两个任务是在 check
以后 timer
以前加入到各自阶段的任务队列中的,那么会先执行 setTimeout
,其余状况会先执行 setImmediate
。
总的来讲,setImmediate
在大部分的状况下会比 setTimeout
先执行。
从技术上来讲,process.nextTick
并不属于 Event Loop
的一部分,它会在每一个阶段执行完毕转入下一个阶段的以前执行。若是有多个 process.nextTick
语句(无论它们是否嵌套),都会在当前阶段结束以后所有执行。
好比:
process.nextTick(function A() {
console.log(1);
process.nextTick(function B() {
console.log(2);
});
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
复制代码
这段代码会输出:“1 => 2 => setTimeout”
再来看下 setImmediate
的
setImmediate(function A() {
console.log(1);
setImmediate(function B() {
console.log(2);
});
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
复制代码
这段代码的老是在最后输出 2,说明 setImmediate
会将它里面的事件注册到下一个循环中。
因为 process.nextTick
里面的 process.nextTick
也会在当前阶段执行,那么若是 process.nextTick
发生了嵌套,那么就会产生无限循环,不再会转入其余阶段。
process.nextTick(function foo() {
process.nextTick(foo);
});
复制代码
node
中的 promise
和 process.nextTick
都属于微任务,它也会在每一个阶段执行完毕以后调用,可是它的优先级会比 process.nextTick
低。