js中的事件循环机制(Event Loop)

我们都知道js是单线程的,js的任务也分为同步任务和异步任务。js先执行同步任务再执行异步任务。

这篇文章主要是讲执行异步任务的过程。

macrotask和microtask

异步任务中,分为macrotask(宏任务)microtask(微任务)两类。 js引擎按照分类分别推入这两个类型的任务队列中,执行顺序如下:

  1. 先取出macrotask任务队列中的第一个任务进行执行
  2. 执行完毕后取出microtask中的所有任务顺序执行
  3. 开始浏览器渲染
  4. 重复前面三个步骤

流程图如下:

'流程图'

macro-task:

  • script (整体代码)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

micro-task:

  • process.nextTick
  • Promise(原生)
  • Object.observe
  • MutationObserver

宏任务都会被推到运行队列里面去,在一次事件循环中根据先进先出的原理顺序执行。

setInterval每次执行完都会把自己放到任务队列的最后面。

而在任务队列中,如果时间到了,就执行,没到的话就先跳过,等下一个时间循环时间到了就立马执行

任务队列

理解事件循环有一个非常重要的概念就是任务队列。

js在运行的过程中,先执行同步任务,遇到异步任务的时候就会先分类。根据宏任务和微任务放入到各自的任务队列中。

而在异步任务中,又上面的介绍可知:

  1. js先在宏任务的任务队列中取出一个宏任务执行。(先进先出)
  2. 执行当前微任务队列中的全部任务,这道题中就是Promise
  3. 浏览器开始渲染
  4. 然后不断重复以上过程,直到任务队列为空

题目

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. 执行同步代码1和7,输出startend。此时: 宏任务任务队列:[2,3,4] / 微任务任务队列:[6]

  2. 执行异步代码,因为一整个代码块属于宏任务并且在上一步已经执行。所以这里执行当前微任务队列中的6,输出promise1promise2。宏任务任务队列:[2,3,4] / 微任务任务队列:[]

  3. 开始下一个事件循环,执行一个宏任务,根据先进先出的顺序,执行代码块2。输出setTimeout1此时: 宏任务任务队列:[3,4] / 微任务任务队列:[]
  4. 执行所有的微任务,但是微任务任务队列为空,跳过。
  5. 开始下一个事件循环,执行代码块3,输出setInterval,并且把本身放入任务队列最后。(重要),此时宏任务任务队列:[4,3] / 微任务任务队列:[]
  6. 微任务任务队列为空,跳过。
  7. 开始下一个事件循环,执行代码块4。先执行同步代码,输出setTimeout2,之后把异步任务分类。此时宏任务任务队列:[3,5] / 微任务任务队列:[8]
  8. 执行全部微任务8,输出promise3
  9. 开始下一个事件循环,执行代码块3,输出setInterval 此时宏任务任务队列:[5] / 微任务任务队列:[]
  10. 微任务任务队列为空,跳过。
  11. 开始下一个事件循环,执行代码块5,执行两个同步任务,输出setTimeout3,并清除interval。
  12. 两个任务队列都为空,脚本结束

附加题

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中的事件循环

题目来源:常见异步笔试题