JavaScript 事件循环机制详解:宏任务、微任务与执行顺序

作者:页面旅途

时间:2026-10-25

浏览:100

本文深入解析JavaScript单线程模型下的事件循环机制,详细区分宏任务与微任务的执行时机。通过Promise、setTimeout等示例代码,阐明同步代码、微任务与宏任务的执行优先级,帮助开发者理清复杂异步逻辑的执行顺序,避免潜在Bug。

文章目录

前言

异步函数同样具备明确的执行顺序。从本质上看,JavaScript是一种单线程语言,无论是在浏览器环境还是Node.js环境中,这一特性均适用。在浏览器中,JavaScript代码的执行与DOM节点的渲染共享同一个线程;当JavaScript代码正在执行时,DOM渲染会被阻塞,反之亦然。若采用同步方式处理,页面可能出现白屏或报错,特别是在涉及耗时较长的网络请求或复杂JavaScript逻辑时。因此,实际开发中通常采用异步机制来解决此类问题。

所谓异步,是指将函数放入独立的异步队列中,从而避免阻塞主线程,使JavaScript能够继续执行后续代码,待同步代码执行完毕后,再处理异步队列中的函数。因此,JavaScript会优先执行完所有同步代码,随后才执行异步代码。尽管异步函数之间均属于异步范畴,但它们内部仍存在相对固定的执行顺序。

异步函数的执行主要依赖事件循环机制,本文重点探讨异步任务的分类(宏任务与微任务)、事件循环的工作原理以及异步函数的具体执行顺序。

宏任务

宏任务(Macro-task),也可简称为任务,通常在下一轮DOM渲染完成后执行。常见的宏任务包括:

  • setTimeout:用于设置一个定时器,在设定的延迟时间到期后执行指定的函数或代码块。需要注意的是,setTimeout并不保证在延迟时间到达后立即执行。只有当执行队列中没有其他待处理函数且调用栈为空时,setTimeout指定的函数才会在延迟到期后执行。

    // setTimeout 延迟执行不等于到期时立即执行
    let now = new Date().getSeconds();
    setTimeout(() => {
        console.log('this is setTimeout 0');
    }, 0);
    setTimeout(() => {
        console.log('this is setTimeout 200');
    }, 200);
    while(true) {
        if (new Date().getSeconds() - now >= 2) {
            console.log('break out while loop');
            break;
        }
    }

    运行结果

  • break out while loop this is setTimeout 0 this is setTimeout 200

    执行逻辑为先同步后异步。setTimeout(() => {}, 0)表示在0毫秒后尝试执行函数,但由于当前执行队列中仍有未处理的while循环,因此必须等待while循环执行完毕后,再根据延迟时间到期情况执行函数。

  • setInterval:用于设置定时器,以固定的时间间隔重复执行某一函数或代码块。使用setInterval时需注意最小延迟时间限制,并确保单次执行时间小于设定的间隔时间。如果执行时间无法确定,建议采用递归调用setTimeout的方式替代。

  • 网络请求:主要指XMLHttpRequest等网络请求操作。

微任务

微任务(Micro-task),在下一轮DOM渲染之前执行,其执行优先级高于宏任务。常见的微任务包括:

  • Promise:表示一个异步操作的最终结果和返回值,可能成功也可能失败。异步函数执行时,返回结果的时间点是不确定的。Promise将异步操作的返回值与函数关联起来,确保在异步执行结束后调用对应的回调函数,并返回操作值。这种机制类似于将异步代码“同步化”处理。
  • queueMicrotask:用于将函数添加到微任务队列中。
console.log('start');
// 微任务队列
Promise.resolve().then(() => {
    console.log('promise then');
});
queueMicrotask(() => {
    console.log('queueMicrotask');
});
console.log('end');

运行结果

start end promise then queueMicrotask

事件循环

由于异步操作的存在,事件循环机制应运而生。如果所有操作均为同步,则事件循环将失去作用。在理解事件循环之前,需补充JavaScript的执行过程:

前言

JavaScript在执行代码时,遇到函数会将其压入调用栈,每一帧存储当前函数的参数和局部变量。当一个函数执行完毕,会从调用栈中弹出,直到栈被清空,程序执行结束。在执行过程中,所需的引用数据均从堆中获取。

在实际开发中,通常同时存在同步代码和异步代码。JavaScript执行时仍从第一行代码开始,遇到函数将其压入栈中,先执行同步操作;若遇到异步函数,则根据其类型,将宏任务加入宏任务队列,微任务加入微任务队列。待同步代码执行完毕后,再开始处理异步操作。

异步操作晚于同步操作执行,且异步操作内部也存在先后顺序。总体而言:

  • 微任务先于宏任务执行
  • 微任务之间按先后顺序执行,宏任务之间按延迟时间顺序执行
  • 微任务在下一轮DOM渲染前执行,宏任务在下一轮DOM渲染之后执行
  • 每个任务的执行都是一次出栈操作,直到栈被清空

微任务比宏任务先执行

console.log('start');
// 宏任务队列
setTimeout(() => {
    console.log('setTimeout');
});
// 微任务队列
Promise.resolve().then(() => {
    console.log('promise then');
});
console.log('end');
// 执行结果
start
end
promise then
setTimeout

微任务在下一轮DOM渲染前执行,宏任务在之后执行

let div = document.createElement('div');
div.innerHTML = 'hello world';
document.body.appendChild(div);
let divList = document.getElementByTagName('div');
console.log('同步任务 length ---', list.length);
console.log('start');
setTimeout(() => {
    console.log('setTimeout length ---', list.length);
    alert('宏任务 setTimeout 阻塞'); // 使用alert阻塞js执行
});
Promise.resolve().then(() => {
    console.log('promise then length ---', list.length);
    alert('微任务 promise then 阻塞);
});
console.log('end');

事件循环

事件循环(event loop)会持续监听是否有异步操作,若有则将其添加到对应的队列中等待执行。例如,在宏任务中添加微任务,或在微任务中添加宏任务,当前任务执行完毕后,可能还会产生新任务加入事件循环。

前言

宏任务与微任务

  • 微任务中创建宏任务

        new Promise((resolve) => {
          console.log('promise 1');
          setTimeout(() => {
            console.log('setTimeout 1');
          }, 500);
          resolve();
        }).then(() => {
          console.log('promise then');
          setTimeout(() => {
            console.log('setTimeout 2');
          }, 0);
        });
        new Promise((resolve) => {
          console.log('promise 2');
          resolve();
        })

    运行结果

  • promise 1 promise 2 promise then setTimeout 2 setTimeout 1

    解析

    JavaScript执行代码时,遇到两个Promise实例,分别将它们加入微任务队列,待同步代码执行完毕。

    在微任务队列中,根据先进先出原则,第一个Promise先执行。执行过程中遇到setTimeout,将其加入宏任务队列,resolve()返回执行结果并执行then回调,事件循环将其继续添加到微任务队列;第一个Promise执行完毕后,执行第二个Promise。

    继续执行微任务队列,直到队列清空。若遇到setTimeout,则将其加入宏任务队列。

    此时宏任务队列中有两个待执行任务,由于第二个setTimeout的延迟时间更短,因此优先执行第二个;若延迟时间相等,则按添加顺序执行。

    继续执行宏任务队列,直到队列清空。

  • 宏任务中创建微任务

        setTimeout(() => {
          console.log('setTimeout 1');
          new Promise((resolve) => {
            console.log('promise 1');
            resolve();
          }).then(() => {
            console.log('promise then');
          })
        }, 500);
        setTimeout(() => {
          console.log('setTimeout 2');
          new Promise((resolve) => {
            console.log('promise 2');
            resolve();
          })
        }, 0);

    运行结果

  • setTimeout 2 promise 2 setTimeout 1 promise 1 promise then

    解析

    JavaScript执行代码时,遇到两个setTimeout,将它们加入宏任务队列,待同步代码执行完毕。

    首先检查微任务队列中是否有待处理任务,初始状态下没有,因此直接执行宏任务队列中的任务。第二个setTimeout为零延迟,需优先执行。执行过程中遇到Promise,将其加入微任务队列。第一个宏任务执行完毕。

    在执行第二个宏任务时,微任务队列中已存在待处理任务,因此需先执行微任务。

    微任务执行完毕且延迟时间到期后,第一个setTimeout开始执行。遇到Promise,将其加入微任务队列。

    执行微任务队列中的Promise,执行完毕后遇到then回调,将其继续加入微任务队列。

    直到所有微任务执行完毕。

  • 宏任务中创建宏任务

        setTimeout(() => {
          console.log('setTimeout 1');
          setTimeout(() => {
            console.log('setTimeout 2');
          }, 500);
          setTimeout(() => {
            console.log('setTimeout 3');
          }, 500);
          setTimeout(() => {
            console.log('setTimeout 4');
          }, 100);
        }, 0);

    运行结果

  • setTimeout 1 setTimeout 4 setTimeout 2 setTimeout 3

    解析

    宏任务中创建宏任务,其执行顺序通常按添加先后顺序进行。对于setTimeout而言,若延迟时间相同,则按添加顺序执行;若延迟时间不同,则按延迟时间大小顺序执行。

  • 微任务中创建微任务

        new Promise((resolve) => {
          console.log('promise 1');
          new Promise((resolve) => {
            console.log('promise 2');
            resolve();
          });
          new Promise((resolve) => {
            console.log('promise 3');
            resolve();
          })
          resolve();
        })

    运行结果

  • promise 1 promise 2 promise 3

    解析

    微任务中创建微任务,其执行顺序通常按添加先后顺序进行。

总结

  • 同步代码直接执行,异步代码加入宏任务队列或微任务队列。
  • 微任务在下一轮DOM渲染前执行,宏任务在下一轮DOM渲染之后执行。
  • 事件循环持续监听。
  • 若存在异步操作,需将关联代码放在异步函数中执行;或将异步函数转为同步操作。
  • 若代码层次复杂,同步与异步代码混杂,务必理清代码的执行顺序,避免因异步处理导致难以察觉的Bug。

参考资料

  • ajax请求是宏任务还是微任务一篇搞定(Js异步、事件循环与消息队列、微任务与宏任务)
  • 深入:微任务与Javascript运行时环境 - Web API 接口参考 | MDN (mozilla.org)
  • 在 JavaScript 中通过 queueMicrotask() 使用微任务 - Web API 接口参考 | MDN (mozilla.org)
  • 并发模型与事件循环 - JavaScript | MDN (mozilla.org)

到此这篇关于JavaScript事件循环的文章就介绍到这了,更多相关JS事件循环内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章