×

JavaScript为啥是单线程运行?

提问者:Terry2025.07.11浏览:75

JavaScript

很多刚接触JavaScript的同学,写代码时总会疑惑“为什么setTimeout里的代码不是立刻执行?”“Promise.then和setTimeout谁先执行?”这些问题背后,其实都和JavaScript的事件循环机制有关,想要彻底搞懂异步代码的执行顺序、解决回调地狱或者优化性能,事件循环是必须跨过去的坎,今天咱们就把事件循环拆碎了讲,从基础概念到实际场景,一步步弄明白它到底怎么运作。

得先从JavaScript的设计场景说起,它诞生是为了在浏览器里操作DOM、做交互,如果同时有两个线程改同一个DOM,浏览器根本不知道听谁的,所以从根上,JavaScript就被设计成**单线程**——同一时间只能执行一个任务,但问题来了:如果遇到网络请求、定时器这种耗时操作,单线程干等着岂不是会“卡壳”?比如点击按钮后发请求,要是等着服务器响应期间页面不能动,用户体验直接崩了,这时候,**异步处理机制**和事件循环就派上用场了。

“调用栈”是干啥的?同步代码咋执行?

咱们写的代码,会被拆成一个个“函数调用”,这些调用会被放进一个叫调用栈(Call Stack)的地方,调用栈是“后进先出”的结构,就像堆盘子,新函数调用往上堆,执行完再往下弹。

举个例子:

function a() {
  function b() {
    console.log('b执行');
  }
  b();
  console.log('a执行');
}
a();

执行顺序是:先把a()压入栈,执行a里的代码时遇到b(),把b()压入栈;b执行完打印“b执行”,b弹出栈;回到a里执行console.log('a执行'),最后a弹出栈,整个过程同步代码全靠调用栈按顺序执行。

异步任务咋处理?得靠“任务队列”和“Web APIs”

但遇到异步任务(比如setTimeoutfetch请求),调用栈不能干等着,否则页面就卡了,这时候JavaScript引擎会把异步任务丢给浏览器的Web APIs(可以理解成浏览器提供的后台线程)处理,等Web APIs把异步操作完成后(比如定时器到时间、请求拿到响应),就会生成一个“任务”,丢到任务队列(Task Queue)里等着被处理。

这里得区分宏任务(Macro Task)微任务(Micro Task)两种队列:

  • 宏任务:可以理解成“大任务”,像setTimeoutsetInterval、DOM渲染、script标签整体执行、requestAnimationFrame这些都属于宏任务。

  • 微任务:“小任务”,优先级比宏任务高,像Promise.then/catch/finallyMutationObserver(监听DOM变化)、queueMicrotask这些属于微任务。

事件循环的核心流程:“执行栈→任务队列”循环

事件循环(Event Loop)的核心逻辑可以简化成:只要调用栈空了,就去任务队列里捞任务执行,但实际流程要分宏任务和微任务的优先级:

  1. 先把当前宏任务(比如整个script脚本)里的同步代码全塞进调用栈执行完,调用栈空了。

  2. 执行所有微任务队列里的任务(因为微任务优先级高,要一次性清完),每个微任务执行时,可能又生成新的微任务,也得接着执行,直到微任务队列为空。

  3. 微任务全处理完后,去执行一个宏任务(比如setTimeout的回调),执行完这个宏任务里的同步代码后,再重复步骤2,处理微任务……循环往复。

举个经典例子理解顺序:

console.log('start');
setTimeout(() => {
  console.log('setTimeout宏任务');
}, 0);
Promise.resolve().then(() => {
  console.log('Promise微任务');
});
console.log('end');

执行步骤拆解:

  • 第一步:调用栈执行同步代码console.log('start') → 打印start

  • 遇到setTimeout,属于宏任务,丢给Web APIs计时,时间到了丢到宏任务队列;

  • 遇到Promise.then,属于微任务,丢到微任务队列;

  • 执行console.log('end') → 打印end

  • 现在调用栈空了,先处理微任务队列:执行Promise.then的回调 → 打印Promise微任务

  • 微任务队列为空后,处理宏任务队列里的setTimeout回调 → 打印setTimeout宏任务

所以最终输出顺序是:startendPromise微任务setTimeout宏任务

宏任务和微任务具体有哪些?

得记清楚常见的任务类型,不然遇到复杂异步代码容易懵:

宏任务(Macro Task)常见类型:

  • setTimeoutsetInterval:定时器类;

  • setImmediate(Node.js环境);

  • DOM渲染、页面重绘;

  • script标签的整体执行(可以理解成第一个宏任务);

  • requestAnimationFrame(虽然和渲染有关,但属于宏任务,不过它的触发时机和屏幕刷新率同步)。

微任务(Micro Task)常见类型:

  • Promisethencatchfinally回调(注意new Promise里的执行器是同步的,只有.then这些才是微任务);

  • MutationObserver:监听DOM节点变化的回调;

  • queueMicrotask:手动创建微任务的API(比如queueMicrotask(() => { ... }));

  • Node.js里的process.nextTick(它比普通微任务优先级更高,属于“nextTick队列”,但浏览器里没有)。

复杂场景下事件循环怎么玩?再看个多层嵌套例子

实际开发中,异步代码经常嵌套,比如Promise里套setTimeout,或者微任务里又生成微任务,看这个例子:

console.log('1');
setTimeout(() => {
  console.log('2');
  Promise.resolve().then(() => {
    console.log('3');
  });
}, 0);
Promise.resolve().then(() => {
  console.log('4');
  setTimeout(() => {
    console.log('5');
  }, 0);
});
console.log('6');

一步步分析执行流程:

  1. 执行同步代码:console.log('1') → 打印1

  2. 遇到setTimeout(宏任务),丢到宏任务队列;

  3. 遇到Promise.then(微任务),丢到微任务队列;

  4. 执行console.log('6') → 打印6

  5. 调用栈空了,处理微任务队列:执行Promise.then的回调 → 打印4
    回调里又遇到setTimeout(宏任务),丢到宏任务队列;

  6. 微任务队列处理完了,现在处理宏任务队列里的第一个宏任务(最开始的setTimeout):
    执行回调里的console.log('2') → 打印2
    然后遇到Promise.then(微任务),丢到微任务队列;

  7. 这个宏任务里的同步代码执行完了,现在处理微任务队列(因为每次宏任务执行完后要清微任务):
    执行刚丢进去的Promise.then回调 → 打印3

  8. 微任务处理完,再处理宏任务队列里的下一个宏任务(步骤5里丢的setTimeout):
    执行回调 → 打印5

所以最终输出顺序是:164235

事件循环对开发有啥实际影响?避坑和优化思路

理解事件循环不是为了背概念,而是解决实际问题:

避免微任务过多阻塞渲染

比如在微任务里做大量DOM操作或者循环计算,因为微任务要一次性执行完才会触发渲染(渲染属于宏任务),如果微任务耗时太长,页面会出现“掉帧”或者交互延迟,举个反面例子:

function loop() {
  queueMicrotask(loop); // 无限创建微任务,页面直接卡死
}
loop();

这种情况要拆分成宏任务,或者用requestIdleCallback(不过兼容性一般)。

合理利用微任务做“即时更新”

比如用户点击按钮后,要先更新状态再触发渲染,用微任务就很合适,因为微任务执行完后,下一个宏任务才是渲染,所以微任务里改DOM属性,能保证渲染时是最新的状态。

定时器不准的原因

setTimeout的回调不是“到时间就执行”,而是“到时间后丢到宏任务队列,等调用栈空了、微任务也空了才执行”,如果前面有大量任务,定时器实际执行时间会比预期晚,所以不能依赖setTimeout做高精度计时(比如动画),优先用requestAnimationFrame

事件循环的关键逻辑

最后帮大家把核心逻辑串起来:

  • JavaScript单线程→同步代码走调用栈→异步任务丢给Web APIs→完成后丢到任务队列(分宏/微任务);

  • 事件循环逻辑:调用栈空了→先清所有微任务→再执行一个宏任务→再清所有微任务→循环

  • 微任务优先级高于宏任务,同个宏任务里的微任务要一次性处理完,再处理下一个宏任务。

把这些逻辑吃透,再遇到异步代码执行顺序的问题,就能像“拆积木”一样一步步分析清楚,平时写代码时,也能更合理地安排异步任务,避免页面卡顿或者逻辑执行顺序出错啦~

您的支持是我们创作的动力!

网友回答文明上网理性发言 已有0人参与

发表评论: