js事件循环机制的原理、宏任务、微任务

93 阅读6分钟

js的事件循环机制

面试高频问题之一。JavaScript是单线程的,意味着他一次只能执行一个任务。为了处理异步操作(如网络请求、定时器等),他依赖于循环机制。事件循环允许javascript执行非阻塞代码,使得如网络请求、文件读写等异步操作能够不被阻塞主线程。

事件循环的机制

1.调用栈(call stack)

事件循环的核心是基于调用栈(Call Stack)的。调用栈是一种数据结构,用于存储代码执行时调用的函数。当一个函数被调用时,它会被推入调用栈中,执行完毕后,再从调用栈中弹出。

2.任务队列(Task Queue/Event Queue)

当异步操作(如setTimeout、Promise、AJAX请求等)完成时,相应的回调函数或解决(resolve)值会被放入任务队列中。任务队列遵循FIFO(先进先出)的原则。

3.事件循环(Event Loop)

事件循环是不断运行的过程,他监视调用栈和任务队列。如果调用栈为空,事件循环会从任务队列中取出一个任务并放入调用栈中执行。这个过程会不断重复,直到任务队列为空。

4.微任务队列(Miscrotask Queue)

在ES6中引入了Promise,与之相关的异步操作完成后会将回调函数放入微任务队列中。微任务队列的优先级高于任务队列。当调用栈为空时,事件循环首先会清空微任务队列中的所有任务,然后再去检查任务队列。

事件循环的工作流程

1.执行全局代码:全局代码被推入调用栈执行。

2.遇到异步操作:如果遇到异步操作(如setTimeout、Promise等),相应的回调或解决值会被放入任务队列或微任务队列。

3.调用栈为空:当调用栈为空时,事件循环开始工作,直到微任务队列为空。

4.处理微任务:首先检查微任务队列,如果有任务则依次执行,直到微任务队列为空。

5.处理宏任务:然后再检查任务队列,如果有任务则取出一个任务放入调用栈执行。

6.重复以上步骤:重复步骤4和5,直到所有任务都被处理完毕。

示例代码

console.log('Start');
setTimeout(() => {
    console.log('Timeout');
}, 0);
Promise.resolve().then(() => {
    console.log('Promise');
});
console.log('End');

//输出顺序:Start =》 End =》Promise =》 Timeout

关键点

  • 调用栈:同步代码立即执行。
  • 微任务队列:微任务优先于宏任务。
  • 任务队列:setTimeout回调在微任务处理完毕后执行。

宏任务

宏任务是由浏览器或JavaScript引擎提供的异步任务,通常包括以下操作:

  • setTimeout和setInterval:定时器回调。
  • I/O操作:如文件读写、网络请求等。
  • DOM事件回调:如点击事件、键盘事件等。
  • requestAnimationFrame:浏览器渲染前的回调。
  • script标签中的代码:整个脚本的执行也是一个宏任务。

特点

  1. 宏任务会被放入任务队列中。
  2. 每次事件循环中,只会执行一个宏任务。
  3. 宏任务的执行优先级低于微任务。

微任务

微任务是指优先级更高的异步任务,通常包括以下操作:

  • Promise的回调:如then、catch、finally。
  • MutationObserver:监听DOM变化的回调。
  • queueMicrotask:显式添加微任务的api。

特点

  1. 微任务会被放入微任务队列中。
  2. 每次事件循环中,会清空整个微任务队列(即执行所有微任务)。
  3. 微任务的优先级高于宏任务。

注意事项

  • 避免微任务嵌套:如果在微任务中继续添加微任务,会导致微任务队列无法清空,从而阻塞事件循环。
  • 宏任务的延迟:setTimeout(fn, 0)并不保证立即执行,因为需要等待当前微任务队列清空。
  • await 会使 async 函数暂停,直至对应的 Promise 状态变为已解决。

一些题目

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}
//输出 5个5;`var` 声明的变量没有块级作用域,`setTimeout` 是异步任务,会在 `for` 循环结束后才执行。此时 `i` 的值已经变为 5,所以每个 `setTimeout` 中的回调函数打印的都是 5。

for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}
//输出0、1、2、3、4;let是块级作用域;每个 `setTimeout` 都会捕获到当前循环的 `i` 值。
function asyncTask1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('Async Task 1 completed');
            resolve();
        }, 2000);
    });
}

function asyncTask2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('Async Task 2 completed');
            resolve();
        }, 1000);
    });
}

async function main() {
    console.log('Main function started');
    await asyncTask1();
    console.log('After Async Task 1');
    const task2Promise = asyncTask2();
    console.log('Async Task 2 is in progress');
    await task2Promise;
    console.log('After Async Task 2');
}

console.log('Before calling main function');
main();
console.log('After calling main function');

执行顺序分析

  1. 同步代码优先执行:在调用main函数之前先执行console.log('Before calling main function');
  2. 调用main函数:内部的console.log('Main function started');同步代码立即执行。
  3. 执行 await asyncTask1()asyncTask1 函数返回一个 Promise,由于使用了 awaitmain 函数会暂停执行,等待 asyncTask1 的 Promise 被解决(resolve)。asyncTask1 内部有一个 setTimeout,它会在 2 秒后执行回调函数。在这 2 秒内,JavaScript 引擎会继续执行后续的同步代码。
  4. 继续执行同步代码:执行console.log('After calling main function');
  5. asyncTask1 的 Promise 被解决:2 秒后,asyncTask1 中的 setTimeout 回调函数执行,输出 Async Task 1 completedasyncTask1 的 Promise 被解决,main 函数继续执行,输出 After Async Task 1
  6. 执行 asyncTask2:调用 asyncTask2 函数,它返回一个 PromiseasyncTask2 内部也有一个 setTimeout,会在 1 秒后执行回调函数。接着输出 Async Task 2 is in progress。然后 main 函数再次暂停,等待 asyncTask2 的 Promise 被解决。
  7. asyncTask2 的 Promise 被解决:1 秒后,asyncTask2 中的 setTimeout 回调函数执行,输出 Async Task 2 completedasyncTask2 的 Promise 被解决,main 函数继续执行,输出 After Async Task 2

输出结果

Before calling main function
Main function started
After calling main function
Async Task 1 completed
After Async Task 1
Async Task 2 is in progress
Async Task 2 completed
After Async Task 2

练习

function delayLog(message, time) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log(message);
            resolve();
        }, time);
    });
}

function simpleSyncTask() {
    console.log('This is a simple sync task');
}

async function asyncFlow() {
    console.log('Async flow starts');
    await delayLog('Delayed log 1 after 1.5 seconds', 1500);
    const secondPromise = delayLog('Delayed log 2 after 1 second', 1000);
    simpleSyncTask();
    await secondPromise;
    console.log('Async flow ends');
}

console.log('Before starting async flow');
asyncFlow();
console.log('After starting async flow');

输出结果

Before starting async flow
Async flow starts
After starting async flow
1.5s后
Delayed log 1 after 1.5 seconds
This is a simple sync task
1s后
Delayed log 2 after 1 second
Async flow ends