JavaScript 事件循环机制详解:从同步异步到宏微任务
本文深入解析JavaScript单线程模型下的事件循环机制,详细阐述同步与异步任务的执行流程。重点对比宏任务与微任务的定义、优先级及执行差异,并通过process.nextTick()等实例代码,逐步拆解复杂异步场景下的输出顺序,帮助开发者掌握异步编程的核心逻辑。
前言
本文旨在梳理对 JavaScript 事件循环机制、同步与异步任务、宏任务及微任务的理解。内容可能存在偏差或错误,欢迎读者指正。
一、事件循环和任务队列产生的原因
JavaScript 采用单线程设计,这具有合理性。若同时处理 DOM 删除与添加,浏览器难以协调。
引用
单线程意味着任务串行执行,后一个任务需等待前一个完成,可能导致长时间等待。然而,Ajax 网络请求、setTimeout 时间延迟及 DOM 事件用户交互等任务并不消耗 CPU,属于空等状态,造成资源浪费。因此引入了异步机制:将任务交给相应的异步模块处理,主线程效率大幅提升,可并行处理其他操作。当异步处理完成且主线程空闲时,主线程读取相应的回调函数进行后续操作,从而最大程度利用 CPU。此时出现了同步执行和异步执行的概念:同步执行是主线程按顺序串行执行任务;异步执行则是 CPU 跳过等待,先处理后续任务(CPU 与网络模块、计时器等并行进行任务)。由此产生了任务队列与事件循环,以协调主线程与异步模块之间的工作。
二、事件循环机制
图解

首先将 JavaScript 代码执行操作分为主线程和任务队列。任何一段 JavaScript 代码的执行均可分为以下步骤:
步骤一:主线程读取 JavaScript 代码,此时为同步环境,形成相应的堆和执行栈; 步骤二:当主线程遇到异步操作时,将异步操作交给对应的 API 进行处理; 步骤三:当异步操作处理完成,推入任务队列中; 步骤四:主线程执行完毕后,查询任务队列,取出一个任务,并推入主线程进行处理; 步骤五:重复步骤二、三、四。
常见的异步操作包括:Ajax 请求、setTimeout,以及类似 onclick 事件等。
三、任务队列
同步和异步任务分别进入不同的执行环境:同步任务进入主线程,即主执行栈;异步任务进入任务队列。
首先,顾名思义,既然是一个队列,那么就遵循FIFO原则。
如上示意图,任务队列存在多个,它们的执行顺序:
同一任务队列内,按队列顺序被主线程取走; 不同任务队列之间,存在着优先级,优先级高的优先获取(如用户 I/O)。
3.1 任务队列的类型
任务队列分为宏任务(macrotask queue)和微任务(microtask queue)。
宏任务主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)。
微任务主要包含:Promise、MutationObserver、process.nextTick(Node.js 环境)。
3.2 两者区别
微任务microtask queue:
(1) 唯一,整个事件循环当中,仅存在一个; (2) 执行为同步,同一个事件循环中的 microtask 会按队列顺序,串行执行完毕;
PS:所以利用 microtask queue 可以形成一个同步执行的环境。
宏任务macrotask queue:
(1) 不唯一,存在一定的优先级(用户 I/O 部分优先级更高) (2) 异步执行,同一事件循环中,只执行一个。
3.3 更细致的事件循环过程
- 一、二、三、步同上
- 主线程查询任务队列,执行 microtask queue,将其按序执行,全部执行完毕;
- 主线程查询任务队列,执行 macrotask queue,取队首任务执行,执行完毕;
- 重复四、五步骤;
先用一个简单的例子加深一下理解:
console.log('1, time = ' + new Date().toString()) // 1.进入主线程,执行同步任务,输出1
setTimeout(macroCallback, 0)// 2. 加入宏任务队列 // 7.开始执行此定时器宏任务,调用macroCallback,输出4
new Promise(function (resolve, reject) {//3.加入微任务队列
console.log('2, time = ' + new Date().toString())//4.执行此微任务中的同步代码,输出2
resolve()
console.log('3, time = ' + new Date().toString())//5.输出3
}).then(microCallback)// 6.执行then微任务,调用microCallback,输出5
//函数定义
function macroCallback() {
console.log('4, time = ' + new Date().toString())
}
function microCallback() {
console.log('5, time = ' + new Date().toString())
}
运行结果:

四、强大的异步专家 process.nextTick()
第一次看见这东西,有点眼熟啊,想了一下好像之前 vue 项目中 用过 this.$nextTick(callback) 当时说的是 当页面上元素被重新渲染之后 才会执行回调函数中的代码,不是很理解,暂时记住吧。

4.1 process.nextTick()在何时调用?
任何时候在给定的阶段中调用 process.nextTick(),所有传递到 process.nextTick() 的回调将在事件循环继续之前解析。
在事件循环中,每进行一次循环操作称为tick,知道了这个之后,对理解这个方法什么时候调用瞬间明白了一些!
再借用别人的例子,加深一下对事件循环的理解吧:
var flag = false // 1. 变量声明
Promise.resolve().then(() => {
// 2. 将 then 任务分发到本轮循环微任务队列中去
console.log('then1') // 8. 执行 then 微任务, 打印 then1,flag 此时是 true 了
flag = true
})
new Promise(resolve => {
// 3. 执行 Promise 里 同步代码
console.log('promise')
resolve()
setTimeout(() => { // 4. 将定时器里的任务放到宏任务队列中
console.log('timeout2') // 11. 执行定时器宏任务 这边指定了 10 的等待时长, 因此在另一个定时器任务之后执行了
}, 10)
}).then(function () {
// 5. 将 then 任务分发到本轮循环微任务队列中去
console.log('then2') // 9. 执行 then 微任务, 打印 then2,至此本轮 tick 结束
})
function f1(f) {
// 1. 函数声明
f()
}
function f2(f) {
// 1. 函数声明
setTimeout(f) // 7. 把`setTimeout`中的`f`放到宏任务队列中,等本轮`tick`执行完,下一次事件循环再执行
}
f1(() => console.log('f为:', flag ? '异步' : '同步')) // 6. 打印 `f为:同步`
f2(() => {
console.log('timeout1,', 'f为:', flag ? '异步' : '同步') // 10. 执行定时器宏任务
})
console.log('本轮宏任务执行完') // 7. 打印
运行结果:

process.nextTick 中的回调是在当前 tick 执行完之后,下一个宏任务执行之前调用的。
官方的例子:
let bar;
// 这个方法用的是一个异步签名,但其实它是同步方式调用回调的
function someAsyncApiCall(callback) { callback(); }
// 回调函数在`someAsyncApiCall`完成之前被调用
someAsyncApiCall(() => {
// 由于`someAsyncApiCall`已经完成,bar没有被分配任何值
console.log('bar', bar); // undefined
});
bar = 1;
使用 process.nextTick:
let bar;
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1
});
bar = 1;
再看一个含有 process.nextTick的例子:
console.log('1'); // 1.压入主线程执行栈,输出1
setTimeout(function () { //2.它的回调函数被加入 宏任务队列中
//7.目前微任务队列为空,所以取出 宏任务队列首项,执行此任务
console.log('2'); // 输出2
process.nextTick(function () { // 16.上一次循环结束,在下一次宏任务开始之前调用,输出3
console.log('3');
})
new Promise(function (resolve) {
//8.执行 此promise的同步任务,输出4,状态变为resolve
console.log('4');
resolve();
}).then(function () {//9.检测到异步方法then,将其回调函数加入 微任务队列中
console.log('5'); // 10. 取出微任务队列首项,也就是这个then的回调,执行,输出5
})
})
process.nextTick(function () { // 11.一次事件循环结束,执行nextTick()的回调,输出6
console.log('6');
})
new Promise(function (resolve) {
//3.执行promise中的同步任务 输出7,状态变为resolve
console.log('7');
resolve();
}).then(function () { //4.检测到异步方法then,将其回调函数加入 微任务队列中
console.log('8'); //6. 主线程执行完毕,取出微任务队列中首项,将其回调函数压入执行栈,输出8
})
setTimeout(function () { //5.它的回调函数 加入 宏任务队列中
//12.此刻,微任务队列为空,开始执行此宏任务
console.log('9'); // 输出9
process.nextTick(function () { // 17.此刻 微任务和宏任务队列都为空了,此次循环自动结束,执行此回调,输出10
console.log('10');
})
new Promise(function (resolve) {
//13. 执行此promise的同步任务,输出11,状态改变
console.log('11');
resolve();
}).then(function () {//14.检测到then异步方法,加入微任务队列
console.log('12');//15.取出微任务队列首项,执行此then微任务,输出12
})
})
运行结果:

此过程步骤详解:
- 首先进入主线程,检测到 log 只是普通函数,压入执行栈,输出 1;
- 检测到 setTimeout 为特殊的异步方法(macrotask),将其交由其他内核模块处理,setTimeout 的回调函数被放入
宏任务(macrotask)队列中; - 检测到 promise 对象以及其中的 resolve 是一般的方法,将其同步任务压入执行栈,输出 7,并且状态改变为 ressolve;
- 检测到刚才的 promise 对象的 then 方法是异步方法,将其交由其他内核模块处理,回调函数被放入
微任务(microtask)队列中; - 又检测到一个 setTimeout 为特殊的异步方法,其回调函数被放入
宏任务(macrotask)队列中; - 此时,主线程空了,开始从任务队列中取,取出 微任务队列首项,也就是第一个 promise 的 then 方法的回调,执行,输出 8;
- 检查此时微任务队列为空,取出宏任务队列首项,也就是第一个 setTimeOut,执行其回调函数,输出 2;
- 在它的回调中碰到一个 promise,执行其同步任务,输出 4,状态改变;
- 然后检测到 then,同上,加入到微任务队列;
- 取出微任务队列首项到主线程执行,也就是刚才的 then,输出 5;
- 此次循环结束,在下一个宏任务开始之前,调用第一个 process.nextTick() 的回调,输出 6;
- 开始下一个宏任务,取出宏任务队列首项,也就是第二个 setTimeout 的回调,将其压入执行栈,输出 9;
- 然后将里面的 promise 对象的同步任务压入执行栈,输出 11,状态改为 resolve;
- 这时又检测到异步 then 方法,同上,将其回调加入 微任务队列;
- 取出微任务队列首项,也就是刚才的 then 回调,输出 12;
- 此次循环结束,在下一次宏任务开始之前执行,process.nextTick() 的回调,输出 3;
- 此时发现 任务队列和主线程都空了,此次事件循环自动结束,执行最后一个 process.nextTick() 的回调,输出 10;
结束!趁着灵光乍现的时候,噼里啪啦赶紧记录下来,后面再检查检查是否有问题,也欢迎各位指出我的错误。
再来分析一个简单的例子:
console.log('0');
setTimeout(() => {
console.log('1');
new Promise(function(resolve) {
console.log('2');
resolve();
}).then(()=>{
console.log('3');
})
new Promise(resolve => {
console.log('4');
for(let i=0;i<9;i++){
i == 7 && resolve();
}
console.log('5');
}).then(() => {
console.log('6');
})
})
- 进入主线程,检测到 log 为普通函数,压入执行栈,输出 0;
- 检测到 setTimeOut 是特殊的异步方法,交给其他模块处理,其回调函数加入 宏任务(macrotask)队列;
- 此时主线程中已经没有任务,开始从任务队列中取;
- 发现为任务队列为空,则取出宏任务队列首项,也就是刚才的定时器的回调函数;
- 执行其中的同步任务,输出 1;
- 检测到 promise 及其 resolve 方法是一般的方法,压入执行栈,输出 2,状态改变为 resolve;
- 检测到这个 promise 的 then 方法是异步方法,将其回调函数加入 微任务队列;
- 紧接着又检测到一个 promise,执行其中的同步任务,输出 4,5,状态改变为 resolve;
- 然后将它的 then 异步方法加入微任务队列;
- 执行微任务队列首项,也就是第一个 promise 的 then,输出 3;
- 再取出为任务队列首项,也就是第二个 promise 的 then,输出 6;
- 此时主线程和任务队列都为空,执行完毕;
代码运行结果:


到此这篇关于 JavaScript 关于事件循环机制的刨析的文章就介绍到这了,更多相关 JavaScript 事件循环机制内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
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
