JavaScript 事件循环:写在前面的代码,为什么反而后执行?
|
zhenglin
2026年10月14日 15:5
本文热度 160
|
JavaScript 事件循环机制
学习 JavaScript 异步执行时,常会碰到这几个问题:
“setTimeout 的延迟设为 0,为什么还不能马上执行?”
“Promise.then 和定时器,谁先执行?”
“await 等待结果时,会不会把整个脚本卡住?”
这些问题都与事件循环(Event Loop)有关。代码的执行顺序,不只取决于写在哪里,还取决于它在什么时候、以什么任务进入队列。
一、单线程与任务队列
JavaScript 在主线程上依次执行代码。它能够操作 DOM,如果多个线程同时修改页面,就可能产生冲突;引入锁又会增加协调成本。
执行脚本时,同步代码直接运行。定时器到时、异步操作完成后,相应回调再等待调度。这里主要涉及两类任务:
定时器的等待不会占住主线程,但它的回调也不会在计时结束时立刻打断当前代码。
二、事件循环的执行顺序
执行当前宏任务,包括其中的同步代码。
清空微任务队列,执行期间新增的微任务也要继续处理。
浏览器根据需要进行页面渲染。
执行下一个宏任务,重复上述过程。
每个宏任务结束后,都要先清空微任务队列,再执行下一个宏任务。
主线程上的 JavaScript 执行和页面渲染不能同时进行。因此,当前代码执行时间过长,页面渲染也会被推迟。
三、Promise 与定时器
console.log(1);
new Promise((resolve) => {
console.log(2);
resolve()
}).then(() => {
console.log(3);
setTimeout(() => {
console.log(4);
}, 0)
})
setTimeout(() => {
console.log(5);
}, 1000)
console.log(6);
输出顺序:
执行过程:
同步执行整段脚本,输出 1、2、6。Promise 构造器中的回调也是同步执行的。
当前脚本结束,执行 then 微任务,输出 3,并注册一个 0ms 定时器。
在这个例子中,0ms 定时器先满足执行条件,输出 4;1000ms 定时器随后输出 5。
两个定时器分别计时,并不是先写的就一定先执行。延迟为 0 也不表示立即执行:回调仍要等当前任务和微任务处理完,才有机会运行。
四、嵌套任务的执行顺序
定时器回调内部,也可能产生新的微任务和宏任务:
console.log(1);
setTimeout(() => {
console.log(2);
new Promise((resolve) => {
console.log(3);
resolve()
}).then(() => {
console.log(4);
setTimeout(() => {
console.log(6);
}, 0)
})
setTimeout(() => {
console.log(5);
}, 0)
}, 1000)
console.log(6);
输出顺序:
整段脚本先输出 1、6。这里的第一个 6 来自末尾的同步代码。
外层定时器执行,输出 2、3,安排 then 微任务,并注册输出 5 的定时器。
外层定时器回调结束,先执行微任务,输出 4,再注册输出 6 的定时器。
两个内部定时器的延迟都是 0,输出 5 的先注册,因此先输出 5,再输出 6。
微任务检查并不只发生在最初的 script 结束时,定时器回调结束后也一样。
五、async / await
5.1 async 的返回值
函数加上 async 后,调用结果总是一个 Promise,即使函数没有显式 return 也是如此。
async 可以单独使用,不一定要写 await;在函数体内使用 await 时,该函数需要声明为 async。加上 async 也不意味着函数里的代码全都异步执行:调用函数后,遇到 await 之前的代码仍然同步执行。
5.2 await 的执行过程
对于 await 后面的函数调用,可以分两步看:
先执行调用:函数中的同步代码立即执行,不会因为前面有 await 就被推迟。
再等待结果:暂停当前 async 函数的后续执行。如果返回的是 Promise,就等待它完成,再通过微任务继续执行 await 之后的代码。
由于上述机制,await 后面紧跟的表达式(即 await xxx 中的 xxx)可以按同步代码理解:先执行、先求值;await 语句之后的代码,则在结果就绪后通过微任务恢复执行。
这与 then 回调有相似之处:等待结果就绪后,后续逻辑通过微任务执行。等待期间,函数外部的同步代码会继续运行。
5.3 await 与函数返回值
函数内部有定时器,并不代表 await 就会等到定时器结束:
function A() {
setTimeout(() => {
console.log('a');
}, 1000)
}
function B() {
console.log('b');
}
async function fn() {
await A()
await B()
}
fn()
输出顺序:
A() 注册定时器后就执行结束,返回值是 undefined。await 可以等待这个普通值,后续代码仍会通过微任务恢复,于是 B() 先输出 b;约一秒后,定时器回调才输出 a。
要等待某个异步操作完成,函数需要返回一个能表示该操作完成的 Promise。 await 等待的是函数的返回结果,不会自动等待函数内部另外启动的定时器。
5.4 完整执行顺序
console.log('script start');
async function async1() {
await async2()
console.log('async1 end');
}
async function async2() {
console.log('async2 end');
}
async1()
setTimeout(() => {
console.log('setTimeout');
}, 0)
new Promise((resolve, reject) => {
console.log('promise');
resolve()
})
.then(() => {
console.log('then1');
})
.then(() => {
console.log('then2');
});
console.log('script end');
输出顺序:
script start
async2 end
promise
script end
async1 end
then1
then2
setTimeout
- 输出 script start,调用 async1。执行到 await async2() 时,先调用 async2,同步输出 async2 end。
- async2 没有写 return,但它是 async 函数,因此返回一个已完成的 Promise。async1 暂停,async1 end 等待微任务阶段再输出。
- 外部脚本继续执行:注册定时器,执行 Promise 构造器中的回调,输出 promise,最后输出 script end。
- 同步代码结束,开始清空微任务队列。先恢复 async1,输出 async1 end,再输出 then1。随后 then2 回调进入微任务队列,也在这一阶段执行。
- 微任务队列清空后,定时器回调执行,输出 setTimeout。
阅读原文:点击这里
该文章在 2026/10/14 15:05:15 编辑过