一道例题引出的血案
提示:(绿色表示node才有的对象)
宏任务(macro task):script(整体代码)、setTimeout、setInterval、setImmediate、 I/O、UI rendering
**微任务(micro task):process.nextTick、 promise、 **Object.observe、MutationObserve
**任务优先级:process.nextTick > promise.then > setTimeout > setImmediate **
例题
console.log("golb1");
setTimeout(function () {
console.log("timeout1");
process.nextTick(function () {
console.log("timeout1_nextTick");
});
new Promise(function (resolve) {
console.log("timeout1_promise");
resolve();
}).then(function () {
console.log("timeout1_then");
});
});
setImmediate(function () {
console.log("immediate1");
process.nextTick(function () {
console.log("immediate1_nextTick");
});
new Promise(function (resolve) {
console.log("immediate1_promise");
resolve();
}).then(function () {
console.log("immediate1_then");
});
});
process.nextTick(function () {
console.log("glob1_nextTick");
});
new Promise(function (resolve) {
console.log("glob1_promise");
resolve();
}).then(function () {
console.log("glob1_then");
});
setTimeout(function () {
console.log("timeout2");
process.nextTick(function () {
console.log("timeout2_nextTick");
});
new Promise(function (resolve) {
console.log("timeout2_promise");
resolve();
}).then(function () {
console.log("timeout2_then");
});
});
process.nextTick(function () {
console.log("glob2_nextTick");
});
new Promise(function (resolve) {
console.log("glob2_promise");
resolve();
}).then(function () {
console.log("glob2_then");
});
setImmediate(function () {
console.log("immediate2");
process.nextTick(function () {
console.log("immediate2_nextTick");
});
new Promise(function (resolve) {
console.log("immediate2_promise");
resolve();
}).then(function () {
console.log("immediate2_then");
});
});
我开始按照之前的经验写出这样的顺序:
golb1 -> lob1_promise -> glob2_promise ->
glob1_nextTick -> glob2_nextTick -> glob1_then -> glob2_then ->
timeout1 -> timeout1_promise -> timeout1_nextTick -> timeout1_then ->
timeout2 -> timeout2_promise -> timeout2_nextTick -> timeout2_then ->
immediate1 -> immediate1_promise -> immediate1_nextTick -> immediate1_then ->
immediate2 -> immediate2_promise -> immediate2_nextTick -> immediate2_then
在node环境下执行结果却是
golb1 -> glob1_promise -> glob2_promise ->
glob1_nextTick -> glob2_nextTick -> glob1_then -> glob2_then ->
timeout1 -> timeout1_promise -> timeout2 -> timeout2_promise ->
timeout1_nextTick -> timeout2_nextTick ->
timeout1_then -> timeout2_then ->
immediate1 -> immediate1_promise -> immediate2 -> immediate2_promise ->
immediate1_nextTick -> immediate2_nextTick -> immediate1_then -> immediate2_then
嗅到一丝不一样的味道的我把浏览器不支持的一些方法注释,然后在浏览器里输出,结果是:
golb1 -> glob1_promise -> glob2_promise ->
glob1_then -> glob2_then ->
timeout1 -> timeout1_promise -> timeout1_then ->
timeout2 -> timeout2_promise -> timeout2_then
这个和我之前的经验写出来的是一样,因此大概知道浏览器和node不一样了 = =,果然学习最重要的是实践,之前查阅的资料都只说了浏览器的事件循环。
正片开始,这个文章不适合新手,因为都是个人的总结,新手可以看看最下边的参考资料
事件循环
javascript从诞生之日起就是一门单线程的非阻塞的脚本语言。
单线程是必要的,也是javascript这门语言的基石,原因之一在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的dom操作。试想一下 如果javascript是多线程的,那么当两个线程同时对dom进行一项操作,例如一个向其添加事件,而另一个删除了这个dom,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,javascript选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。
而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。
当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。
js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。
图中的stack表示我们所说的执行栈,web apis则是代表一些异步事件,而callback queue即事件队列。
macro task与micro task
- 宏任务(macro task):script(整体代码)、setTimeout、setInterval、setImmediate、 I/O、UI rendering
- **微任务(micro task):process.nextTick、 promise、 **Object.observe、MutationObserve
- **任务优先级:process.nextTick > promise.then > setTimeout > setImmediate **
注:MutationObserve:监视对DOM树所做更改的能力。
- setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
// setTimeout中的回调函数才是进入任务队列的任务
setTimeout(function() {
console.log('xxxx');
})
// 非常多的同学对于setTimeout的理解存在偏差。所以大概说一下误解:
// setTimeout作为一个任务分发器,这个函数会立即执行,而它所要分发的任务,也就是它的第一个参数,才是延迟执行
- 来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
- 事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
node事件循环
node事件循环分为6个阶段
外部输入数据–>轮询阶段(poll)–>检查阶段(check)–>关闭事件回调阶段(close callback)–>定时器检测阶段(timer)–>I/O 事件回调阶段(I/O callbacks)–>闲置阶段(idle, prepare)–>轮询阶段(按照该顺序反复运行)…
- timers 阶段:这个阶段执行 timer(setTimeout、setInterval)的回调
- I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调
- idle, prepare 阶段:仅 node 内部使用
- poll 阶段:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里
- ** check 阶段:执行 setImmediate() 的回调**
- close callbacks 阶段:执行 socket 的 close 事件回调
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
说明:
- 一开始执行栈的同步任务(这属于宏任务)执行完毕后(依次打印出 start end,并将 2 个 timer 依次放入 timer 队列),会先去执行微任务(这点跟浏览器端的一样),所以打印出 promise3
- 然后进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;这点跟浏览器端相差比较大,timers 阶段有几个 setTimeout/setInterval 都会依次执行,并不像浏览器端,每执行一个宏任务后就去执行一个微任务
注意
- setTimeout 和 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 阶段去执行回调了。
- 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
总结
- 浏览器环境下,microtask 的任务队列是每个 macrotask 执行完之后执行,
(浏览器)微任务是跟屁虫,一直跟着当前宏任务后面,代码执行到一个微任务就跟上,一个接一个 - Node.js 中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务。
参考资料
详解JavaScript中的Event Loop(事件循环)机制
深入核心,详解事件循环机制
浏览器与Node的事件循环(Event Loop)有何区别?