Skip to content

事件循环

浏览器中 JavaScript 的执行流程和 Node.js 中的流程都是基于 事件循环 的。

事件循环 的概念非常简单。它是一个在 JavaScript 引擎等待任务,执行任务和进入休眠状态等待更多任务这几个状态之间转换的无限循环。

理解事件循环

因为 js 是单线程运行的,在代码执行时,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码时,如果遇到异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当异步事件执行完毕后,再将异步事件对应的回调加入到一个任务队列中等待执行。

微任务与宏任务

任务队列可以分为宏任务队列(macrotask)微任务队列(microtask),每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。

  • 微任务包括: promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。
  • 宏任务包括: script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲染等。

来看一个示例:

javascript
setTimeout(() => console.log('timeout'))

Promise.resolve().then(() => console.log('promise'))

console.log('code')

这里的执行顺序是怎样的?

  • code 首先显示,因为它是常规的同步调用。

  • promise 第二个出现,因为 then 会通过微任务队列,并在当前代码之后执行。

  • timeout 最后显示,因为它是一个宏任务。

TIP

微任务仅来自于我们的代码。 另外还有一个特殊的函数 queueMicrotask(func),它对 func 进行排队,以在微任务队列中执行。 你可以在MDN查看关于 queueMicrotask 的更多细节。

事件循环执行顺序

执行顺序示意图

1.进入到 script 标签,就进入到了第一次事件循环.

2.遇到同步代码,立即执行

3.遇到宏任务,放入到宏任务队列里.

4.遇到微任务,放入到微任务队列里.

5.执行完所有同步代码

6.执行微任务代码

7.微任务代码执行完毕,本次队列清空

8.寻找下一个宏任务,重复步骤 1

以此反复直到清空所有宏任务 ✅

思考题

所有代码运行的结果都有规律可循

必要时,可借助画图软件,模拟宏任务、微任务的入队、出队流程 🔄,环环相扣,水到渠成。

  • 以下代码的输出顺序是?
javascript
console.log(1)

setTimeout(() => console.log(2))

Promise.resolve().then(() => console.log(3))

Promise.resolve().then(() => setTimeout(() => console.log(4)))

Promise.resolve().then(() => console.log(5))

setTimeout(() => console.log(6))

console.log(7)
查看答案 👀

输出结果为:1 7 3 5 2 6 4,你答对了吗?

让我们一步步推导:

javascript
console.log(1)
// 第一行立即执行,它输出 `1`。
// 到目前为止,宏任务队列和微任务队列都是空的。

setTimeout(() => console.log(2))
// `setTimeout` 将回调添加到宏任务队列。
// - 宏任务队列中的内容:
//   `console.log(2)`

Promise.resolve().then(() => console.log(3))
// 将回调添加到微任务队列。
// - 微任务队列中的内容:
//   `console.log(3)`

Promise.resolve().then(() => setTimeout(() => console.log(4)))
// 带有 `setTimeout(...4)` 的回调被附加到微任务队列。
// - 微任务队列中的内容:
//   `console.log(3); setTimeout(...4)`

Promise.resolve().then(() => console.log(5))
// 回调被添加到微任务队列
// - 微任务队列中的内容:
//   `console.log(3); setTimeout(...4); console.log(5)`

setTimeout(() => console.log(6))
// `setTimeout` 将回调添加到宏任务队列
// - 宏任务队列中的内容:
//   `console.log(2); console.log(6)`

console.log(7)
// 立即输出 7

总结一下

  • 立即输出数字 17,因为简单的 console.log 调用没有使用任何队列。
  • 然后,主代码流程执行完成后,开始执行微任务队列。
    • 其中有命令行:console.log(3); setTimeout(...4); console.log(5)。
    • 输出数字 35,setTimeout(() => console.log(4)) 将 console.log(4) 调用添加到了宏任务队列的尾部。
    • 现在宏任务队列中有:console.log(2); console.log(6); console.log(4)。
  • 当微任务队列为空后,开始执行宏任务队列。并输出 264

最终,我们得到的输出结果为:1 7 3 5 2 6 4

  • 以下代码的输出顺序是?
javascript
const promise = new Promise((resolve, reject) => {
  console.log(1)
  setTimeout(() => {
    console.log('timerStart')
    resolve('success')
    console.log('timerEnd')
  }, 0)
  console.log(2)
})
promise.then(res => {
  console.log(res)
})
console.log(4)
查看答案 👀

输出结果为

javascript
1
2
4
timerStart
timerEnd
success

你答对了吗? 代码执行过程如下:

  1. 首先遇到 Promise 构造函数,会先执行里面的内容,打印 1
  2. 遇到定时器 setTimeout,它是一个宏任务,放入宏任务队列;
  3. 继续向下执行,打印出 2
  4. 由于 Promise 的状态此时还是 pending,所以 promise.then 先不执行;
  5. 继续执行下面的同步任务,打印出 4
  6. 此时微任务队列没有任务,继续执行下一轮宏任务,执行 setTimeout;
  7. 首先执行 timerStart,然后遇到了 resolve,将 promise 的状态改为 resolved 且保存结果并将之前的 promise.then 推入微任务队列,再执行 timerEnd
  8. 执行完这个宏任务,就去执行微任务 promise.then,打印出 resolve 的结果 success
  • 以下代码的输出顺序是?
javascript
Promise.resolve().then(() => {
  console.log('promise1')
  const timer2 = setTimeout(() => {
    console.log('timer2')
  }, 0)
})
const timer1 = setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(() => {
    console.log('promise2')
  })
}, 0)
console.log('start')
查看答案 👀

输出结果为

javascript
start
promise1
timer1
promise2
timer2

你答对了吗?代码执行过程如下:

  1. 首先,Promise.resolve().then 是一个微任务,加入微任务队列
  2. 执行 timer1,它是一个宏任务,加入宏任务队列
  3. 继续执行下面的同步代码,打印出start
  4. 这样第一轮宏任务就执行完了,开始执行微任务 Promise.resolve().then,打印出 promise1
  5. 遇到 timer2,它是一个宏任务,将其加入宏任务队列,此时宏任务队列有两个任务,分别是 timer1、timer2;
  6. 这样第一轮微任务就执行完了,开始执行第二轮宏任务,首先执行定时器 timer1,打印 timer1
  7. 遇到 Promise.resolve().then,它是一个微任务,加入微任务队列
  8. 开始执行微任务队列中的任务,打印 promise2
  9. 最后执行宏任务 timer2 定时器,打印出 timer2
  • 以下代码的输出顺序是?
javascript
const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})
const promise2 = promise1.then(() => {
  throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
  console.log('promise1', promise1)
  console.log('promise2', promise2)
}, 2000)
查看答案 👀

输出结果为

javascript
promise1 Promise {<pending>}
promise2 Promise {<pending>}

Uncaught (in promise) Error: error!!!

promise1 Promise {<fulfilled>: "success"}
promise2 Promise {<rejected>: Error: error!!}

你答对了吗?代码执行过程如下:

  1. 进入 promise1,执行该构造函数中的代码,把 setTimeout 加入宏任务队列
  2. 把 promise2 中的 promise1.then 加入微任务队列
  3. 按顺序执行,打印 promise1 和 promise2,它们的状态依然是 pending,因此,首先输出的内容是:promise1 Promise {<pending>}promise2 Promise {<pending>}
  4. 紧接着是一个 setTimeout,加入宏任务队列
  5. 主代码流程执行完成后,开始执行微任务队列,抛出错误 Uncaught (in promise) Error: error!!! ,此时 promise2 的状态变为 rejected
  6. 执行宏任务队列,promise1 的状态变为 fulfilled,然后打印 promise1、promise2:promise1 Promise {<fulfilled>: "success"}promise2 Promise {<rejected>: Error: error!!}
  • 以下代码的输出顺序是?
javascript
async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
  setTimeout(() => {
    console.log('timer1')
  }, 0)
}
async function async2() {
  setTimeout(() => {
    console.log('timer2')
  }, 0)
  console.log('async2')
}
async1()
setTimeout(() => {
  console.log('timer3')
}, 0)
console.log('start')
查看答案 👀

输出结果为

javascript
async1 start
async2
start
async1 end
timer2
timer3
timer1

你答对了吗?代码执行过程如下:

  1. 首先进入 async1,打印出 async1 start
  2. 之后遇到 async2,进入 async2,遇到定时器 timer2,加入宏任务队列,之后打印 async2
  3. 由于 async2 阻塞了后面代码的执行,所以执行后面的定时器 timer3,将其加入宏任务队列,之后打印 start
  4. 然后执行 async2 后面的代码,打印出 async1 end,遇到定时器 timer1,将其加入宏任务队列;
  5. 最后,宏任务队列有三个任务,先后顺序为 timer2timer3timer1,没有微任务,所以直接所有的宏任务按照先进先出的原则执行。
  • 以下代码的输出顺序是?
javascript
async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2')
}

console.log('script start')

setTimeout(function () {
  console.log('setTimeout')
}, 0)

async1()

new Promise(resolve => {
  console.log('promise1')
  resolve()
}).then(function () {
  console.log('promise2')
})
console.log('script end')
查看答案 👀

输出结果为

javascript
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

你答对了吗?代码执行过程如下:

  1. 开头定义了 async1 和 async2 两个函数,但是并未执行,执行 script 中的代码,所以打印出 script start
  2. 遇到定时器 setTimeout,它是一个宏任务,将其加入到宏任务队列;
  3. 之后执行函数 async1,首先打印出 async1 start
  4. 遇到 await,执行 async2,打印出 async2,并阻断后面代码的执行,将后面的代码加入到微任务队列;
  5. 然后跳出 async1 和 async2,遇到 Promise,打印出 `promise1;
  6. 遇到 resolve,将其加入到微任务队列,然后执行后面的 script 代码,打印出 script end
  7. 之后就该执行微任务队列了,首先打印出 async1 end ,然后打印出 promise2
  8. 执行完微任务队列,就开始执行宏任务队列中的定时器,打印出 setTimeout
  • 以下代码的输出顺序是?
javascript
const first = () =>
  new Promise((resolve, reject) => {
    console.log(3)
    let p = new Promise((resolve, reject) => {
      console.log(7)
      setTimeout(() => {
        console.log(5)
        resolve(6)
        console.log(p)
      }, 0)
      resolve(1)
    })
    resolve(2)
    p.then(arg => {
      console.log(arg)
    })
  })
first().then(arg => {
  console.log(arg)
})
console.log(4)
查看答案 👀

输出结果为

javascript
3
7
4
1
2
5
Promise{<resolved>: 1}

你答对了吗?代码执行过程如下:

  1. 首先进入 first 中的 Promise,打印 3,接着进入 p 中的 Promise,打印 7
  2. 遇见 setTimeout,加入宏任务队列
  3. 执行 Promise p 中的 resolve,状态变为 resolved,返回值为 1
  4. 执行 Promise first 中的 resolve,状态变为 resolved,返回值为 2;
  5. 将 p.then()加入微任务队列
  6. 将 first.then()加入微任务队列
  7. 向后执行,打印 4
  8. 执行微任务,先后打印 12
  9. 执行宏任务,打印 5,由于 Promise p 已经变为 resolved 状态,所以 resolve(6)不会生效,因此打印的 p 的结果为Promise{<resolved>: 1}
  • 以下代码的输出顺序是?
javascript
const async1 = async () => {
  console.log('async1')
  setTimeout(() => {
    console.log('timer1')
  }, 2000)
  await new Promise(resolve => {
    console.log('promise1')
  })
  console.log('async1 end')
  return 'async1 success'
}
console.log('script start')
async1().then(res => console.log(res))
console.log('script end')
Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .catch(4)
  .then(res => console.log(res))
setTimeout(() => {
  console.log('timer2')
}, 1000)
查看答案 👀

输出结果为

javascript
script start
async1
promise1
script end
1
timer2
timer1

你答对了吗?代码执行过程如下:

  1. 首先执行同步代码,打印出 script start
  2. 执行 async1, 打印 async1 ,遇到定时器 timer1 将其加入宏任务队列
  3. 之后是执行 Promise,打印出 promise1,由于 Promise 没有返回值,所以后面的代码不会执行
  4. 然后执行同步代码,打印出 script end
  5. 继续执行下面的 Promise,.then 和.catch 期望参数是一个函数,这里传入的是一个数字,因此就会发生值渗透,将 resolve(1)的值传到最后一个 then,直接打印出 1
  6. 遇到第二个定时器,将其加入到微任务队列,执行微任务队列,按顺序依次执行两个定时器,但是由于定时器时间的原因,会在先打印出 timer2,后打印出timer1
  • 以下代码的输出顺序是?
javascript
setTimeout(function () {
  console.log(1);
}, 100);

new Promise(function (resolve) {
  console.log(2);
  resolve();
  console.log(3);
}).then(function () {
  console.log(4);
  new Promise((resove, reject) => {
    console.log(5);
    setTimeout(() =>  {
      console.log(6);
    }, 10);
  })
});
console.log(7);
console.log(8);
查看答案 👀

输出结果为

javascript
2
3
7
8
4
5
6
1

除了要靠考虑入队的顺序,还要注意定时器的时间。