js中的事件循环机制(Event Loop)
我们都知道js是单线程的,js的任务也分为同步任务和异步任务。js先执行同步任务再执行异步任务。
这篇文章主要是讲执行异步任务的过程。
macrotask和microtask
异步任务中,分为macrotask(宏任务)
和microtask(微任务)
两类。
js引擎按照分类分别推入这两个类型的任务队列中,执行顺序如下:
- 先取出macrotask任务队列中的第一个任务进行执行
- 执行完毕后取出microtask中的所有任务顺序执行
- 开始浏览器渲染
- 重复前面三个步骤
流程图如下:
macro-task:
- script (整体代码)
- setTimeout
- setInterval
- setImmediate
- I/O
- UI rendering
micro-task:
- process.nextTick
- Promise(原生)
- Object.observe
- MutationObserver
宏任务都会被推到运行队列里面去,在一次事件循环中根据先进先出的原理顺序执行。
setInterval
每次执行完都会把自己放到任务队列的最后面。
而在任务队列中,如果时间到了,就执行,没到的话就先跳过,等下一个时间循环时间到了就立马执行
任务队列
理解事件循环有一个非常重要的概念就是任务队列。
js在运行的过程中,先执行同步任务,遇到异步任务的时候就会先分类。根据宏任务和微任务放入到各自的任务队列中。
而在异步任务中,又上面的介绍可知:
- js先在宏任务的任务队列中取出一个宏任务执行。(先进先出)
- 执行当前微任务队列中的全部任务,这道题中就是Promise
- 浏览器开始渲染
- 然后不断重复以上过程,直到任务队列为空
题目
console.log('start')
setTimeout(() => {
console.log('setTimeout1');
},0);
const myInterval = setInterval(() => {
console.log('setInterval');
},0)
setTimeout(() => {
console.log('setTimeout2');
Promise.resolve().then(() => {
console.log('promise3');
})
setTimeout(() => {
console.log('setTimeout3');
clearInterval(myInterval);
},0)
},0)
Promise.resolve()
.then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
})
console.log('end');
运行结果:
start
end
promise1
promise2
setTimeout1
setInterval
setTimeout2
promise3
setInterval
setTimeout3
题目
为什么Promise先于setTimeout执行?
解释这道题目前,有一个棘手的问题必须要先解决,那就是:
按照js事件循环机制,会先执行一个宏任务再执行所有的微任务。那么setTimeout属于宏任务,为什么Promise比setTimeout先执行。
先吐槽:看了许多csdn博客和简书(真的是程序员届的毒瘤),不得不说有些解释都是无理无据不能让人信服的。有些甚至存在误导。我也是踩了许多坑,根据对网上言论的判断,总结了一下自己的理解。
我们看到宏任务中script
整体代码属于一个宏任务。那么在我们这一整个大代码块中,都属于==一个==宏任务。根据js运行机制,先执行同步代码。执行完之后执行异步代码。那么这一整个宏任务已经执行了,下一次要执行当前微任务任务队列中的全部任务。所以这个时间Promise执行了,setTimeout要下一次时间循环时候才会执行了。
题目解释:
有了上面的理论基础,我们开始真正的解题。为了方便表述,根据上面的代码做了标记,每个数字代表各自对应的代码块。(下一个事件循环之前,浏览器会执行一个渲染,因为和解题无关因此忽略)
执行顺序:
执行同步代码1和7,输出
start
和end
。此时: 宏任务任务队列:[2,3,4] / 微任务任务队列:[6]执行异步代码,因为一整个代码块属于宏任务并且在上一步已经执行。所以这里执行当前微任务队列中的6,输出
promise1
和promise2
。宏任务任务队列:[2,3,4] / 微任务任务队列:[]- 开始下一个事件循环,执行一个宏任务,根据先进先出的顺序,执行代码块2。输出
setTimeout1
此时: 宏任务任务队列:[3,4] / 微任务任务队列:[] - 执行所有的微任务,但是微任务任务队列为空,跳过。
- 开始下一个事件循环,执行代码块3,输出
setInterval
,并且把本身放入任务队列最后。(重要),此时宏任务任务队列:[4,3] / 微任务任务队列:[] - 微任务任务队列为空,跳过。
- 开始下一个事件循环,执行代码块4。先执行同步代码,输出
setTimeout2
,之后把异步任务分类。此时宏任务任务队列:[3,5] / 微任务任务队列:[8] - 执行全部微任务8,输出
promise3
- 开始下一个事件循环,执行代码块3,输出
setInterval
此时宏任务任务队列:[5] / 微任务任务队列:[] - 微任务任务队列为空,跳过。
- 开始下一个事件循环,执行代码块5,执行两个同步任务,输出
setTimeout3
,并清除interval。 - 两个任务队列都为空,脚本结束
附加题
从github
上摘抄了三道变式题,大同小异。大家可以自测下看自己有没有掌握。
变式一:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
//async2做出如下更改:
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise3');
resolve();
}).then(function() {
console.log('promise4');
});
console.log('script end');
运行结果:
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
变式二:
async function async1() {
console.log('async1 start');
await async2();
//更改如下:
setTimeout(function() {
console.log('setTimeout1')
},0)
}
async function async2() {
//更改如下:
setTimeout(function() {
console.log('setTimeout2')
},0)
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout3');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
运行结果:
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
变式三
async function a1 () {
console.log('a1 start')
await a2()
console.log('a1 end')
}
async function a2 () {
console.log('a2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise1')
})
a1()
let promise2 = new Promise((resolve) => {
resolve('promise2.then')
console.log('promise2')
})
promise2.then((res) => {
console.log(res)
Promise.resolve().then(() => {
console.log('promise3')
})
})
console.log('script end')
运行结果:
script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
await和promise的顺序有误?
v8引擎在不同版本中,await后面的函数和promise的执行顺序会有变化。 因为最新的EcmaScript 标准已经改过来了。即:先promise后执行await后面的函数。
个人实验中,node v8.x 和最新的v12.x是正确的结果,中间的几个版本是颠倒的。 chrome 71和chrome73结果也不一样。文中的答案也是按照最新的标准写的。 这里不做过多解释,只要知道有这么一回事就行了。
补充
异步队列执行的时候,此次事件循环中如果时间到了就执行,没到了就先跳过。直到下一次事件循环自己的时间到了就马上执行。
参考文章:js中的事件循环
题目来源:常见异步笔试题