同步任务
当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。这个过程反复进行,直到执行栈中的代码全部执行完毕。

这个同步代码的执行过程可以是无限进行下去的,除非发生了栈溢出,即超过了所能使用内存的最大值。
异步任务
js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码...如此反复

事件队列
我们先看两个实验
// 实验一
console.log('进入页面')
setTimeout(() => {
console.log('setTimeout1')
}, 0)
new Promise((resolve) => {
console.log('promise')
setTimeout(() => {
console.log('setTimeout2')
resolve()
}, 0)
}).then(() => {
console.log('then')
})
console.log('代码执行结束')
/* 打印结果
进入页面
promise
代码执行结束
setTimeout1
setTimeout2
then
*/
// 实验二
console.log('进入页面')
setTimeout(() => {
console.log('setTimeout1')
}, 0)
new Promise((resolve) => {
console.log('promise')
resolve()
}).then(() => {
console.log('then')
})
console.log('代码执行结束')
/*打印结果
进入页面
promise
代码执行结束
then
setTimeout1
*/
上面的两个demo中,demo2里在Promise里使用了setTimeout包了一层。造成then方法中的回调函数执行顺序稍有不同。一个在setTimeout之后,一个在setTimeou之前。为什么会造成这样的结果?
因为事件分为 macro-task
、micro-task
。不同的事件会进入不同的队列。
也就是说事件队列对应的也有两个,macro queue
和 micro queue
。
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
上文中提到当主线程闲置后,会去事件队列中查找执行回调函数,这个是有一个先后策略的,先micro queue
后macro queue
。而这里的promise中只有在执行了resolve()
后才会向micro queue
中push在then
方法里写的回调函数。这也是上边两个实验中console.log('then')
执行顺序不同的原因,下面是详细的执行顺序:
实验1:
// 主线程开始执行同步代码
进入页面
//遇到setTimeout1异步挂起,接着执行同步代码
promise
//遇到setTimeout2异步挂起,接着执行同步代码
代码执行结束
// 同步代码执行完毕,主线程闲置,检查micro queue为空,检查macro queue 有两个回调函数
setTimeout1
setTimeout2
//执行了resolve(),向micro queue中push回调函数
//同步代码执行完毕,主线程闲置检查micro queue,执行里边的回调函数
then
实验2:
// 主线程开始执行同步代码
进入页面
//遇到setTimeout1异步挂起,接着执行同步代码
promise
//执行了resolve(),向micro queue中push回调函数
代码执行结束
// 同步代码执行完毕,主线程闲置,检查micro queue发现有回调函数执行
then
// 同步代码执行完毕, 主线程闲置, 检查micro queue为空,检查macro queue发现有回调函数并执行
setTimeout1
总结
不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到宏任务队列中第一个任务执行,然后再执行所有的微任务队列。
- 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是
异步语句(比如setTimeout等); - 全局Script代码执行完毕后,调用栈Stack会清空;
- 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
- 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
- microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
- 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
- 执行完毕后,调用栈Stack为空;
- 重复第3-7个步骤;
- 重复第3-7个步骤;
- ......
参考文章:
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
https://juejin.im/post/59e85eebf265da430d571f89
https://juejin.im/post/5b8f76675188255c7c653811
网友评论