# 事件循环
# 事件循环的定义
JavaScript的代码执行模式是单线程,它是通过调用栈来执行函数的,但除此之外,还要依靠任务队列来完成其他代码的执行。整个执行的过程,我们称之为事件循环。任务队列是分有两类的,一类为宏任务队列,另一类为为任务队列。在最新的标准中,他们又称为task与jobs。
# 浏览器任务的分类
# 宏任务有以下几种
- setTimeout
- setInternal
- setImmediate
- I/O
- UI render
- script(整体代码)
# 微任务有以下几种
- Promise
- async/await
- process.nextTick
- MutationObserver(html5新特性)
# 浏览器事件循环执行机制
任务队列中,宏任务与微任务之间的执行顺序如下:
- 执行宏任务(整体script段),过程中遇到其他宏任务,放入宏任务队列。
- 执行微任务,若执行过程中产生了新的微任务,则放入微任务队列。
- 执行之前产生的新的微任务。
- 执行宏任务队列。
- 两个队列都为空,执行完毕
总体流程就是,执行宏1(产生宏1.1,push macro queue),执行微1(产生微1.1,push micro queue)。执行微2结束(micro queue为空),执行宏2(macro queue为空)。执行完毕。
`宏任务(整块代码)->微任务->子微任务->子宏任务->执行完毕
# 例题
# 题1
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
执行顺序:
- 宏1(script 片段)
- console语句,输出 script start
- 微1(async2),此时执行环境是async1,输出 async2 end,保留async1上下文。
- 宏1.1(setTimeout) 推入宏队列
- 微2(Promises),输出 Promise,产生微2.1(promise1)并 推入微队列
- console语句,输出 script end
- 微任务队列不为空,执行微队列,微2.1(promise1),输出 promise1. 产生微2.1.1(promise2)并推入微队列
- 微2.1.1(promise2),输出 promise2
- 微队列完,执行权回到async1上下文,输出 async end
- 执行宏队列,宏1.1(setTimout),输出 setTimeout
理论与实践不一致
----理论上----
//script start
//async2 end
//Promise
//script end
//promise1
//promise2
async end 原位置
//setTimeout
---实际上---
//script start
//async2 end
//Promise
//script end
async end 这一步发生了改变
//promise1
//promise2
//setTimeout
TIP
可以看到 async end的打印,理论和实践上有偏差。但是在题2中,理论又和实践一致。为什么会这样,将在问题萌生与解决中说明
# 题2
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
return Promise.resolve().then(()=>{
console.log('async2 end1')
})
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
执行顺序如下:
输出script start
执行async1中的async2。输出async2 end, 将 返回的promise推入微队列
setTimeout推入宏队列
输出Promise, promise1推入微队列
输出script end
输出 async2 end1
输出promise1,将promise2推入微队列
输出promise2
执行权返回至async1,输出async1 end
微队列为空,执行宏队列,输出setTimeout
理论与实践一致,浏览器、node运行也是如此 // script start // async2 end // promise // script end // async2 end1 // promise1 // promise2 // async1 end // setTimeout
# 问题的萌生与解决
上面两道题中,执行的逻辑判断都逐一说明,但题1的返回结果与理论中不同。于是我比对了两题的不同:题2与题1的差别,其实就在于题2中的async2函数多了个异步返回值。关键点就在于这个异步返回值,我猜测将会影响async中await后续代码的执行顺序。我在上述两题的基础上,新增两个例子,并验证我的猜测。
# 示例1,所有async不带有异步返回值。
console.log("script start");
async function async1() {
await async2();
console.log("async1 end");
}
async function async2() {
await async3();
console.log("async2 end");
}
async function async3() {
console.log("async3 end");
}
async1();
setTimeout(function () {
console.log("setTimeout");
}, 0);
new Promise((resolve) => {
console.log("Promise");
resolve();
})
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});
console.log("script end");
//script start
//async3 end
//Promise
//script end
async2 end --可以看到在第一轮从上到下执行完毕时,紧跟的是async2,async2后跟着的时promise,可以说明async2在第一轮执行判断时,被注册到微队列中。
promise1 -- 打印出promise1的这一轮循环,async1函数内的 console.log("async1 end")同样被注册到微队列中。
async1 end -- 执行上一轮给到的微任务。
//promise2
//setTimeout
可以注意到,async在没有返回值时,await后面的命令被视为微任务注册到队列中。并且,他们的顺序是按照 await async3()之后,再到await async2()之后,每一轮呈逆序注册。
# 示例2,所有async带有异步返回值。
console.log("script start");
async function async1() {
await async2();
console.log("async1 end");
}
async function async2() {
await async3();
console.log("async2 end");
}
async function async3() {
console.log("async3 end");
return Promise.resolve().then(() => {
console.log("async3 end1");
});
}
async1();
setTimeout(function () {
console.log("setTimeout");
}, 0);
new Promise((resolve) => {
console.log("Promise");
resolve();
})
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});
console.log("script end");
//script start
//async3 end
//Promise
//script end
async3 end1 异步返回值被注册为微任务
//promise1
//promise2
async2 end 带有异步返回值后,await后的命令将被视作同步语句,且也是逆序执行。
async1 end 带有异步返回值后,await后的命令将被视作同步语句,且也是逆序执行。
//setTimeout
在带有异步返回值后,await后的语句不再被视为微任务,于是排到了promise2(最后一个微任务)之后。
# Node中的事件循环
浏览器中有事件循环,node 中也有,事件循环是 node 处理非阻塞 I/O 操作的机制,node中事件循环的实现是依靠的libuv引擎。由于 node 11 之后,事件循环的一些原理发生了变化,这里就以新的标准去讲,最后再列上变化点让大家了解前因后果。
# node任务的分类
宏任务有下列:
- setTimeout
- setInterval
- setImmediate
- script(整体代码)
- I/O操作等。
微任务有下列:
- process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)
- new Promise().then(callback)等。
# Process.nextTick
process.nextTick 是一个独立于 eventLoop 的任务队列。
在每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行。
# node事件循环执行机制
node中的事件循环执行机制与其版本有关。node v11之前的版本与浏览器无异。但在v11之后(包括v11)的版本,如果宏任务(setTimeout、setInternal、setImmediate等)中有微任务,将执行里面的微任务队列(但如果有process.nextTick,则先于微任务执行)。之后再往下执行。
# node例1
setImmediate(() => {
console.log('timeout1')
Promise.resolve().then(() => console.log('promise resolve'))
process.nextTick(() => console.log('next tick1'))
});
setImmediate(() => {
console.log('timeout2')
process.nextTick(() => console.log('next tick2'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
node v11之前
//timeout1
//timeout2
//timeout3
//timeout4
//next tick1
//next tick2
//promise resolve
node v11之后
//timeout1
//next tick1
//promise resolve
//timeout2
//next tick2
//timeout3
//timeout4
# node例2
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
node v11之前
// timer1
// timer2
// promise1
// promise2
node v11之后 但要看第一个定时器执行完后,第二个定时器是否在执行队列中
第二个定时器还没执行完,则实际上只有第一个定时器进行事件循环。
// timer1
// promise1
// timer2
// promise2
第二个定时器执行完,则按照v11之后的执行机制来,与浏览器执行机制一致。
// timer1
// timer2
// promise1
// promise2
# node和浏览器eventLoop的主要区别
两者最大的区别在于node的微任务执行顺序取决于node的版本,而浏览器则保持着恒定的执行顺序。
# 总结
- 当存在async await微任务时,注意async的返回值是否为异步,若为异步,则执行结果将有相应的变化。
- node的微任务执行顺序取决于node的版本在11之前,还是执行之后。
- 事件循环的执行机制大体为:宏-微-宏,微任务有产生子微任务,则宏-微-微-宏,以此类推。