详解JS事件循环及宏任务微任务的原理
本文深入解析JavaScript单线程模型下的事件循环机制,详细区分宏任务与微任务的执行优先级。通过代码示例阐明setTimeout、Promise及DOM渲染在事件循环中的具体行为,帮助开发者理清异步代码执行顺序,避免潜在的逻辑Bug。
从本质上看,JavaScript是一种同步、阻塞且单线程的语言,无论是在浏览器环境还是Node.js环境中皆如此。在浏览器中,JavaScript代码的执行与DOM节点的渲染共用同一个线程。这意味着当JavaScript代码正在执行时,DOM渲染会被挂起;反之,当浏览器进行DOM渲染时,JavaScript代码的执行也会被暂停。如果完全按照这种同步模式运行,页面极易出现白屏或报错现象,尤其是在处理耗时较长的网络请求或复杂JavaScript逻辑时。因此,在实际开发中,通常采用异步机制来规避这些问题。
那么,什么是异步?JavaScript代码是按顺序逐行执行的,当遇到alert这类阻塞性代码时,JavaScript引擎会暂停后续代码的执行,直到该阻塞代码执行完毕。而异步机制则是将函数放入独立的异步队列中,从而避免阻塞主线程,使JavaScript能够继续向下执行。只有当同步代码全部执行完毕后,JavaScript才会去处理异步队列中的函数。因此,JavaScript的执行顺序总是先完成同步代码,再执行异步代码。
异步任务还可以进一步细分,每个异步函数可被视为一个待执行的任务,主要分为宏任务和微任务两类。本文的重点在于介绍浏览器环境下的事件循环机制,而Node.js环境下的事件循环将在后续文章中探讨。
宏任务
宏任务(Macrotask),也可以简单理解为普通任务,它会在下一轮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等网络请求操作。
微任务
微任务(Microtask)在下一轮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在执行代码时,遇到函数调用会将其压入调用栈(Call Stack)。每一帧调用栈都会存储当前函数的参数和局部变量。当一个函数执行完毕,它将从调用栈中弹出。直到调用栈被清空,程序才算执行完毕。在执行过程中,所需的引用数据均从堆(Heap)中获取。
在实际开发中,代码通常混合了同步和异步操作。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。
到此这篇关于详解JS事件循环及宏任务微任务的原理的文章就介绍到这了,更多相关JS宏任务 微任务内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
JS动态构建复杂嵌套JSON对象的方法
2026-11-05 15:41:52
JS负数取余为什么是负数?详解余数与模运算区别
2026-11-05 13:34:41
JS 解压出现乱码或报错怎么解决?
2026-11-05 12:32:38
JS 中 startsWith、indexOf 与正则判断字符串前缀的对比
2026-11-04 22:19:43
JS解析Excel后怎么将数据转换为JSON格式
2026-11-04 20:58:28
