首页/文章/八股文

面试必问:为什么Promise比setTimeout先执行

2025-07-20
324911 分钟
...

一、 进程与线程

在聊事件循环机制之前,我们先来聊一下进程与线程,这样有助于我们更好地理解事件循环机制。

1.进程与线程的基本概念

1.进程

  • 定义:进程是操作系统分配资源(如内存、CPU)的基本单位,每个进程独立运行且拥有独立的内存空间
  • 前端场景:浏览器中每个标签页通常对应一个独立的渲染进程,负责HTML、CSS、JavaScript的解析和页面渲染。
  • 特点:进程间严格隔离,一个崩溃不会影响其他进程(如Chrome崩溃的标签页不会导致整个浏览器关闭)

2.线程

  • 定义:线程是进程内的执行单元,共享进程的资源(如内存、文件句柄),是CPU调度的最小单位。
  • 前端场景:渲染进程内包含多个协作线程,如JS引擎线程GUI渲染线程等,共同完成页面交互和渲染

通俗地说,进程就像是一个工厂,而线程就是工厂中不同的流水线。

2.前端中的单线程与异步机制

JavaScript采用单线程模型,但通过事件循环(Event Loop)​实现异步操作。V8引擎在运行js代码时,这个进程中只有一个线程会被开启。

二、什么是事件循环机制

在了解事件循环机制之前我们先来看一段代码,大家可以先思考下最后会输出啥

let a = 1
console.log(a);
setTimeout(function() {
  let b = 2
  console.log(b);
  a++
  setTimeout(function() {
    b++
  }, 2000)
  console.log(b);
}, 1000)
console.log(a);

这段代码最后会输出1、1、2、2,因为我们先定义了一个变量a,然后执行到第2行开始打印a输出第一个1,然后我们在第3行碰到了一个定时器,这个定时器是异步的,根据V8的规则我们先去执行同步代码再去执行异步代码。所以我们先跳过它去执行最后一行又输出第二个1,最后一行执行完毕后我们开始执行定时器里的代码,里面定义了一个变量b值为2,随后打印第一个2,随后a++,a的值变为2,然后又遇到一个定时器,同样的,我们先去执行同步代码打印b,输出第二个2。最后2秒后执行定时器里的代码b的值变为3

其实,我们刚刚思考这段代码的过程就是JS的事件循环机制,浏览器的V8引擎在执行一份JS代码时,会循环往复地执行,先执行不耗时的代码,再执行耗时的代码,在耗时代码中也是先执行同步代码,再执行异步代码。直到所有代码执行完毕。

所以事件循环机制是V8按照先执行同步代码,再执行异步代码,如此反复执行的一种策略

我们再来看一段代码

let a = 1
for (var i = 0; i < 10000; i++) {
  a++
}
console.log(a);

大家觉得这份代码耗时吗?如果是执行1亿次循环耗时吗?其实,这份代码是不耗时的,因为只要你的电脑性能足够好,这份代码的执行就不需要耗费时间,而类似上面定时器这种代码,不管你的电脑性能好不好,都需要耗费时间。

三、事件循环机制的执行步骤

1. 宏任务与微任务的定义

宏任务

  • 定义:由宿主环境(如浏览器)发起的异步任务,需要较长时间执行,每次事件循环处理一个宏任务。
  • 常见类型
    • setTimeout / setInterval
    • DOM 事件回调(点击、滚动等)
    • I/O 操作(文件读写、网络请求)
    • requestAnimationFrame(浏览器动画帧回调)
    • script标签
    • 页面渲染

微任务

  • 定义:由JavaScript引擎自身发起的轻量级异步任务,需在当前宏任务结束后立即执行,优先级高于宏任务。
  • 常见类型
    • Promise.then() / .catch() / .finally()
    • MutationObserver(监听 DOM 变化)
    • queueMicrotask() 显式添加的微任务

2. 执行顺序与事件循环流程

事件循环机制由以下三部分构成:

1. 调用栈(Call Stack)

  • 用于执行同步代码(如 console.log()、函数调用),遵循“后进先出”(LIFO)原则。
  • 同步代码会立即执行,直到栈空。

2. 任务队列(Task Queues)

  • 宏任务队列Macrotask Queue):存放 setTimeoutsetIntervalDOM事件I/O操作等回调。
  • 微任务队列Microtask Queue):存放Promise.then()MutationObserverqueueMicrotask()等回调。

3. 事件循环调度器

  • 持续检查调用栈和任务队列,按优先级调度任务

执行流程

  1. 执行同步代码:所有同步任务直接进入调用栈执行,直到栈空。
  2. 清空微任务队列:检查微任务队列,依次执行所有微任务(包括执行过程中新产生的微任务)。
  3. 执行一个宏任务:从宏任务队列中取出首个任务执行。
  4. 重复循环:每执行完一个宏任务后,再次清空微任务队列,依次循环。

3. 特殊的await

我们不能把await单纯的看成.then来对待,await封装了.then和其他东西,它不完全等同于.then,我们在遇到await时要特别注意以下几点。

  • 浏览器对await的执行提前了 (await后面的代码当成同步代码来执行)
  • await会将后续代码(下一行/下N行代码)挤入微任务队列

四、 代码示例与分步解析

我们来看一个例子,大家可以思考下这段代码的输出结果。

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

这份代码最后会一次输出1 2 7 3 5 4 6

我们来解释下,首先我们执行同步代码先输出1,然后我们碰到了一个new Promise的函数调用,注意,除了我们刚刚提到的常见的宏任务的和微任务,其他代码绝大部分都是同步代码,这里的new Promise也是。所以里面执行new Promise里面的代码,输出2,然后Promise的状态变为成功的状态,然后就碰到了.then我们先不执行它因为它是微任务,并且V8引擎会开始创建一个微任务队列,.then入队。要注意这个.then里面的setTimeout是先不管的,因为.then都不调用。然后我们又遇到了一个setTimeout,它属于一个宏任务,v8引擎开始创建一个宏任务队列,并且这个setTimeout入队。随后我们执行同步代码输出7。此时所有的同步代码都执行完毕,就开始去检查有没有异步代码要执行,检查的顺序是先检查微任务队列,再检查宏任务队列。于是就把之前进入微任务队列里的.then拿出来执行了,.then的执行上下文此时才进入调用栈中,.then开始执行便先输出了3,输出3之后又碰到一个宏任务setTimeout,继续把这个定时器放到宏任务队列,此时宏任务队列有两个setTimeout。 此时微任务队列已经清空,开始执行宏任务队列,首先执行第一个入队的setTimeout,执行它的过程中先输出5,又遇到一个setTimeout,将它加入宏任务队列,接着执行第二个setTimeout,输出4,然后执行最后一个setTimeout,输出6

大家如果还是不明白可以看我画的图再理解一下(图中的数字代表输出顺序,而不是输出结果)。

 

 

 

 

 

我简单的总结下流程:就是先执行同步代码,中途如果遇到异步代码先不执行把它们挂到队列中(宏任务挂宏任务队列,微任务挂微任务队列),等到所有同步代码都执行完毕后去执行异步代码,先执行微任务队列中的代码,再执行宏任务队列中的代码。

 

相比各位对事件循环机制比较清晰了,我们再来看一段代码,大家思考下这段代码的输出结果是什么。

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 startasync2 endpromisescript endasync1 endthen1then2setTimeout。下面是分析过程:

首先我们执行同步代码打印script start,然后遇到了用async声明的一个函数,这相当于这个函数里面写了return new Promise,注意这不是异步,仍然是同步代码。这只是个函数体的声明并不执行,后面又遇到一个async,依旧是个函数体的声明,不执行。随后我们碰见了async1这是个函数调用,我们开始执行async1函数,因为我们前面提到await后面的代码要当成同步代码来执行,所以我们立即执行async2,所以输出了async2 end,又因为await的下几行代码要进入微任务队列,所以async1 end先不打印,随后我们遇到了一个定时器这是个宏任务,我们先不执行,并把它加入到宏任务队列。后面我们遇到了一个new Promise这是个同步代码,我们输出promise,随后我们遇到了两个.then我们把它们加入到微任务队列。最后输出script end,所有同步代码都执行完毕之后,我们去执行异步代码,我们先去清空微任务队列,依次输出async1 endthen1then2,然后清空宏任务队列输出setTimeout

五、setTimeout定时器执行的时间准吗?

setTimeout被执行时,浏览器会启动一个新的线程来计时,等到时间结束才将定时器的回调取出来执行(js 主线程将其取出),如果此时JS主线程还在执行同步代码,那么该回调就会一直挂起,直到同步代码执行完毕,微任务也执行完毕才执行该回调(就算原本宏任务队列里面有代码也是先执行该定时器的回调)

六、总结

  • 核心规则:同步代码 → 微任务 → 宏任务 → 渲染 → 下一轮循环。
  • 应用场景
    • 使用 Promise 处理即时异步操作(如数据请求)。
    • 使用 setTimeout 调度延迟任务(如动画分帧)。
  • 优化建议:避免在微任务中执行耗时操作,合理拆分任务以平衡性能与响应速度。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
八股文2026-08-27
定时器按顺序播放多个音频,切后台音频会乱
本身不会补触发,但 它会被系统级冻结 ——iOS Safari 后台完全停止计时,切回前台后 只补执行一次 (不是不补,而是"缺失的中间状态补不上")。间隔短的不会出大问题, 间隔长的会出现"音频流被压缩" ——切回来后本该播 5 分钟的间隙,实际只过了 3 分钟,结果整段对不上。 核心原则 :定时器只能用来"提醒一次", 真正决定"该不该播"的是绝对时间戳 。 setTimeout 在后台会发生什么(按平台) | 平台 | 后台行为...
面试
八股文2026-08-27
线上项目白屏的原因
白屏的本质是 渲染管线某一环断了 ——可能是资源、JS、接口、路由、样式、兼容性任一环节出问题。排查按"控制台 → 网络 → DOM → 环境"四步定位。 本质(前端类比) 把网页想成一栋楼: 白屏 ≠ 一定是同一种原因——这是面试想听的层次。 六大类原因(按出现频率) | 类别 | 典型表现 | 真实案例 | | : | : | : | | ① 资源加载失败 | DOM 是空的 | 入口 JS 404、CDN 挂了、CSS 阻塞 |...
面试
八股文2026-08-27
背景图就是 1MB 大图,怎么优化
1MB 大背景图优化分 三步 :压缩体积(10 30x)、按需加载(按设备/视口/网速)、渲染期优化(GPU 合成)。背景图跟 不同——它是 CSS,不走浏览器的原生懒加载机制,得手动处理。 背景图 vs 的关键区别(先讲清楚这个) 这是面试官想听的"针对性认知"——背景图不是普通图片,不能套通用方案。 三步优化(按优先级) 第 1 步:压缩体积(最重要,立竿见影) 1MB 的来源一般是这几种 ,对应解决方案: | 原始问题 | 体积来...
面试
八股文2026-08-27
项目里很多图片和视频,怎么优化
图片视频优化分 四层 :网络层(CDN/格式)、加载层(懒加载/预加载)、渲染层(解码/缓存)、业务层(按需/降级)。面试要把这四层都讲到位才算有体系。 四层优化模型 第 1 层:网络层(省钱、省时间) 核心目标:让资源体积小、让用户拿到资源快 | 手段 | 作用 | 关键点 | | : | : | : | | 图片格式 | WebP/AVIF 比 JPEG 小 25 50% | 兼容 fallback | | 视频格式 | H.265...
面试
八股文2026-08-20
Embedding 向量模型:从语义表示到相似度计算
前言 大模型「读懂」文字靠的是 token,但 token 之间只有离散的编号关系,模型并不知道「苹果」和「李子」在语义上很近。要让程序能「理解」两段文字的相似程度,必须先把文本映射成一个 高维向量 ,再用几何方法比较。这一步就是 Embedding。 本篇基于我本地 今天的真实代码,从语义表示讲到余弦相似度,并复盘几个踩过的真实坑。 一、为什么需要 Embedding 传统关键词检索是「字面匹配」: Embedding 做的是「语义匹...
面试
八股文2026-08-19
前端面试100题
前端面试 100 题 适用方向:中高级前端 / React / Vue / Next.js / Nuxt / TypeScript / 工程化 / 实时通信 / Electron / Node.js / AI 应用前端 使用方式:优先掌握“标准回答”,再练“面试官追问”,最后把“结合你的简历怎么答”组织成自己的项目故事。 说明 “结合你的简历怎么答”只使用你简历中已经出现的项目与技术事实。 “标准回答 / 追问”属于通用前端知识总结,用...
面试

评论

请登录后发表评论

去登录
加载评论中...

目录