跳到主要内容

事件循环

Event Loop

JavaScript 是单线程的。

  1. 调用栈

用于执行函数调用的栈结构,遵循「后进先出」的原则。每当一个函数被调用时,它会被推入调用栈顶;当函数执行完毕后,它会从栈顶弹出。

  1. 消息队列

存储了待处理的消息。每个消息都关联了一个回调函数,这些回调函数会在将来的某个时间点被执行。遵循「先进先出」的原则。

  1. 事件循环

主要任务是协调调用栈和消息队列之间的工作:

  • 检查调用栈:事件循环会不断检查调用栈是否为空。如果调用栈不为空,则继续执行栈中的函数。
  • 处理消息队列:如果调用栈为空,事件循环会从消息队列中取出第一个消息,并将其回调函数推入调用栈,开始执行。

事件循环机制

  1. 首先开始执行主线,从上往下执行所有的同步代码
  2. 在执行过程中如果遇到宏任务就存放到宏任务队列中,遇到微任务加入微任务队列,然后主线往下执行,直到主线执行完毕
  3. 查看微任务队列中是否存在微任务,如果存在,则将所有微任务也按主线方式执行完成,然后清空微任务队列
  4. 开始将宏任务队列中的第一个宏任务设置为主线继续执行,执行完一个宏任务,会去查看微任务队列,接着立即执行所有的微任务,直到微任务队列为空。然后再进行下一个宏任务,直到清空所有的宏任务队列

每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务。

script 标签是宏任务,宏任务先执行。

宏任务

常见的宏任务有:

  • script 脚本
  • 定时器任务(setTimeout、setInterval、setImmediate(Node.js 环境))
  • I/O 操作
  • 浏览器事件(如 click、mouseover 等)
  • 页面渲染(如 回流或重绘)
  • 网络请求

setTimeout 并不是直接把回调函数放进异步队列中去,而是在定时器的时间到了之后才放进去。如果此时这个队列已经有很多任务了,那就排在它们的后面。

console.log(1);
setTimeout(() => {
console.log(2);
}, 3000);
setTimeout(() => {
console.log(4);
}, 1000);
console.log(3);

结果是:1 3 4 2

微任务

常见的微任务有:

  • Promise 的then/catch/finally方法
  • process.nextTick (Node.js 环境)
  • MutaionOberver

注意:

  • new Promise()是同步任务
  • 在 node.js 环境中,process.nextTick 的优先级高于微任务

练习题

示例 1:

console.log(1);
setTimeout(() => {
console.log(2);
}, 6000);
console.log(3);

const button = document.querySelector('button');

button.addEventListener('click', () => {
console.log(4);
});

console.log(5);

如果在定时器时间 6s 内点击,则输出顺序是:1、3、5、4、2,否则输出顺序是:1、3、5、2、4


示例 2:

console.log(1);

setTimeout(() => {
console.log(2);
}, 0);

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

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

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

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

console.log(7);

答案是:1 7 3 2 5 6 4

解析:

  1. 首先执行同步代码,输出 1、7。将其他代码按照分类放入宏任务队列或微任务队列
  2. 执行微任务队列,输出 3,将 setTimeout(() => console.log(4)) 放入宏任务队列(排在 6 的后面),清空微任务队列
  3. 执行宏任务队列,先输出 2,微任务队列为空,继续下一轮循环
  4. 执行宏任务队列,执行 Promise.resolve().then(() => console.log(5)) ,将 5 放入微任务队列,主线执行完后,去执行微任务队列,输出 5
  5. 继续循环宏任务队列,依次输出 6 和 4

img


示例 3:

async function async1() {
console.log('async1 start');
await async2();
console.log('asnyc1 end');
}

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

console.log('script start');

setTimeout(() => {
console.log('setTimeOut');
}, 0);

async1();

new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});

console.log('script end');

答案:

script start
async1 start
async2
promise1
script end
asnyc1 end
promise2
setTimeOut

解析:

async 函数中的 await 实际上会把后面的代码转换为 Promise 的 then 回调

首先,主线程执行同步代码:

  • 定义 async1 和 async2 函数,但此时不会执行函数体。
  • 执行 console.log('script start'); 输出第一个结果:script start。
  • 调用 setTimeout,这是一个宏任务,回调函数会被放到宏任务队列中,等待所有同步代码和微任务执行完毕后执行。
  • 调用 async1(),进入 async1 函数体。

在 async1 函数内部:

  • console.log('async1 start'),这是同步代码,所以输出第二个结果:async1 start。
  • await async2(); 这里会先调用 async2 函数,然后处理 await 后的逻辑。

进入 async2 函数:

  • console.log('async2'),同步代码,输出第三个结果:async2。
  • async2 函数返回一个 Promise,因为是 async 函数,默认返回 resolved 状态的 Promise。

await 会让出执行权,后面的代码会被包装成微任务。所以,在 async1 中,await 后面的代码 console.log('asnyc1 end') 会被放入微任务队列中,等待当前同步代码执行完毕后执行。

接下来继续主线程的同步代码:

  • new Promise 里的执行器函数是同步执行的,所以 console.log('promise1') 会被立即执行,输出第四个结果:promise1。
  • resolve()被调用,此时 Promise 状态变为 resolved,后面的 then 回调被放入微任务队列。
  • 最后执行 console.log('script end'),输出第五个结果:script end。

此时,主线程的同步代码全部执行完毕,开始处理微任务队列中的任务。当前的微任务队列中有两个任务:一个是 await async2() 之后的回调(输出 asnyc1 end),另一个是 Promise 的 then 回调(输出 promise2)。

微任务队列的执行顺序是先进先出,所以先处理第一个微任务:

  • 执行 console.log('asnyc1 end'),输出第六个结果:asnyc1 end。
  • 然后是第二个微任务,执行 console.log('promise2'),输出第七个结果:promise2。

当所有微任务执行完毕后,事件循环会检查宏任务队列,此时有一个 setTimeout 的回调,执行它:

  • 输出 console.log('setTimeOut'),第八个结果:setTimeOut。

示例 4:

setTimeout(() => {
console.log(1);
}, 0);

asyncFn();

const promise = new Promise((resolve, reject) => {
console.log(2);
reject();
})
.then(console.log.bind(null, 3), console.log.bind(null, 4))
.catch(console.log.bind(null, 5));

async function asyncFn() {
const res1 = await 6;
console.log(res1);
const res2 = await new Promise((resolve, reject) => {
console.log(7);
});
console.log(8);
}

console.log(9);

答案:

2
9
6
7
4 undefined
1

解析:

  • await 6 将 6 包装为 Promise.resolve(6),后续代码被推入微任务队列
  • reject() 触发 .then 的第二个参数(非 .catch),因此 5 不会输出
  • await new Promise(...) 中的 Promise 没有返回状态,即是 pending 状态,await 会一直等待,导致 8 永远不会输出
  • reject()未传递拒绝原因,相当于 reject(undefined).then的第二个参数会接收拒绝原因作为第一个参数,即 console.log.bind(null, 4)实际执行的是 console.log(4, undefined)