事件循环是js运行的核心,js是单线程的,js的异步事件就是依赖于事件循环机制

从这张图中就能看到js的事件执行机制,首先js代码有一个执行栈,所有的js代码首先都被压栈,在执行代码过程中,如果遇到一些异步代码(比如setTimeout,ajax,promise.then以及用户点击等操作),那么浏览器就会将这些代码放到另一个线程(在这里我们叫做幕后线程)中去执行,在前端由浏览器底层执行,在 node 端由 libuv 执行,这个线程的执行不阻塞主线程的执行,主线程继续执行栈中剩余的代码。

当幕后线程(background thread)里的代码执行完成后(比如setTimeout时间到了,ajax请求得到响应),该线程就会将它的回调函数放到任务队列(又称作事件队列、消息队列)中等待执行。而当主线程执行完栈中的所有代码后,它就会检查任务队列是否有任务要执行,如果有任务要执行的话,那么就将该任务放到执行栈中执行。如果当前任务队列为空的话,它就会一直循环等待任务到来。因此,这叫做事件循环。

务队列
那么,问题来了。如果任务队列中,有很多个任务的话,那么要先执行哪一个任务呢?
其实(正如上图所示),js是有两个任务队列的,一个叫做 Macrotask Queue(Task Queue) 大任务, 一个叫做 Microtask Queue 小任务

Macrotask 常见的任务:

  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • 用户交互操作,UI渲染

Microtask 常见的任务:

  • Promise(重点)
  • process.nextTick(nodejs)
  • Object.observe(不推荐使用)

其实事件循环执行流程如下:

  1. 检查 Macrotask 队列是否为空,若不为空,则进行下一步,若为空,则跳到3
  2. 从 Macrotask 队列中取队首(在队列时间最长)的任务进去执行栈中执行(仅仅一个),执行完后进入下一步
  3. 检查 Microtask 队列是否为空,若不为空,则进入下一步,否则,跳到1(开始新的事件循环)
  4. 从 Microtask 队列中取队首(在队列时间最长)的任务进去事件队列执行,执行完后,跳到3

其中,在执行代码过程中新增的microtask任务会在当前事件循环周期内执行,而新增的macrotask任务只能等到下一个事件循环才能执行了。

简而言之,一次事件循环只执行处于 Macrotask 队首的任务,执行完成后,立即执行 Microtask 队列中的所有任务。

值得一提的是,js主线程中一开始跑的任务就是宏任务,所以当第一次执行完执行栈中所有任务,打算开始事件循环的时候,会从微任务队列中取

//执行代码
 console.log(1)
setTimeout(function() {
  //settimeout1
  console.log(2)
}, 0);
const intervalId = setInterval(function() {
  //setinterval1
  console.log(3)
}, 0)
setTimeout(function() {
  //settimeout2
  console.log(10)
  new Promise(function(resolve) {
    //promise1
    console.log(11)
    resolve()
  })
  .then(function() {
    console.log(12)
  })
  .then(function() {
    console.log(13)
    clearInterval(intervalId)
  })
}, 0);

//promise2
Promise.resolve()
  .then(function() {
    console.log(7)
  })
  .then(function() {
    console.log(8)
  })
console.log(9)
//执行结果
1
9
7
8
2
3
10
11
12
13

参考网址

补充

在微任务中,nextTick的优先级永远比Promise.then的优先级要高,所以执行先后有差异

有async和await情况下的事件循环

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

async和await实际是语法糖,而真正的写法其实是await之后的promise作为一个新的promise的resolve函数的参数,同时将之后的代码包裹在then之中,这样就可以做到等待目前执行的promise返回结果之后再继续向下执行

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
// 其实就是
async function async1() {
    console.log('async1 start');
    Promise.resolve(async2()).then(()=>console.log('async1 end'))
}

所以这里的执行顺序是

async1 start
async2 //async2()作为参数会立刻执行,
promise1
script end
async1 end
promise2