一、JS 执行机制一句话总览(先背,面试开篇直接用)
JS 是单线程的,但浏览器不是;JS 负责“执行”,浏览器负责“调度”。
核心支撑就是:Event Loop(事件循环) —— 简单说,就是浏览器帮JS协调“同步任务”和“异步任务”的执行顺序,避免单线程卡死。
二、为什么 JS 是单线程?(面试官高频开篇题)
面试官问:为什么 JS 设计成单线程?
标准回答思路(直接背,不啰嗦)
-
JS 的核心用途是操作 DOM(比如增删改页面元素);
-
如果设计成多线程,多个线程同时修改同一个 DOM,会出现竞态条件(数据/页面状态不一致);
-
所以 JS 在执行层面被设计成单线程,避免DOM操作冲突。
关键区分(必记):
- JS 是单线程(只有一个执行主线程);
- 浏览器是多线程(有专门处理异步、渲染的线程);
- 异步 ≠ 多线程(异步是“委托浏览器执行,执行完通知JS”,不是JS自己多线程跑)。
三、执行 JS 的“三大件”(必记,面试画图能快速加分)
脑子里一定要刻住这3个东西,理解它们的关系,Event Loop就懂了一半:
1️⃣ Call Stack(调用栈)—— JS的“执行工作台”
通俗说:就是JS当前正在执行的函数,都存在这里,遵循 后进先出(LIFO) 规则(先调用的函数后结束,后调用的先结束)。
function a() {
b()
}
function b() {
c()
}
function c() {}
a() // 执行此函数,调用栈变化如下
执行栈顺序(可视化,好记):
a(调用a,压入栈)
a → b(a里调用b,b压入栈)
a → b → c(b里调用c,c压入栈)
a → b(c执行完,弹出栈)
a(b执行完,弹出栈)
(a执行完,弹出栈,调用栈清空)
2️⃣ Web APIs(浏览器提供的“工具包”)
重点:不是JS本身的内容,是浏览器额外提供的异步能力
常见的Web APIs(记熟,面试常考):
-
定时器:setTimeout、setInterval
-
DOM事件:click、scroll、resize等
-
网络请求:Ajax(XMLHttpRequest)、fetch
-
动画相关:requestAnimationFrame
通俗理解:JS自己处理不了异步(单线程),就把异步任务“委托”给浏览器的Web APIs线程执行,执行完后通知JS。
3️⃣ Task Queues(任务队列)—— 异步任务的“等待区”
浏览器的Web APIs执行完异步任务后,不会直接让JS执行,而是把任务放到“任务队列”里,等待调用栈清空后,再由Event Loop调度到调用栈执行。
任务队列分两类(核心重点,面试必考区分):
🔹 宏任务队列(Macrotask)—— “大任务”,优先级低
常见宏任务(必记):
-
setTimeout、setInterval
-
I/O操作(比如读取文件、网络请求响应)
-
UI事件(click、resize等)
-
script标签(整个JS脚本的执行,属于宏任务)
🔹 微任务队列(Microtask)—— “小任务”,优先级高
常见微任务(必记,易错点):
-
Promise.then / catch / finally(重点:Promise本身是同步的,只有then/catch/finally回调是微任务)
-
MutationObserver(监听DOM变化的API)
-
queueMicrotask(专门创建微任务的方法)
四、Event Loop 的真实执行规则(核心中的核心,必背)
标准顺序(记死,面试直接说,比通俗解释更加分)
- 执行 Call Stack(调用栈)中的所有同步代码,直到调用栈清空;
- 调用栈清空后,执行当前所有微任务(直到微任务队列清空,哪怕微任务里又产生新微任务,也一并执行);
- 微任务队列清空后,执行一次UI渲染(如果浏览器需要渲染页面,比如DOM有变化);
- 渲染完成后,从宏任务队列中取出一个宏任务,放入调用栈执行;
- 回到第3步,循环往复(这就是“事件循环”)。
一句话简化版(方便快速回忆):
同步 → 微任务 → 渲染 → 宏任务(循环)
五、经典面试例子(逐行拆解,理解后直接背输出顺序)
面试常考“输出顺序题”,掌握这3个例子,同类题都能搞定,每道题重点记“执行逻辑”和“最终输出”。
🌰 例子 1:同步 + Promise + setTimeout(基础必考题)
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then(() => {
console.log(3)
})
console.log(4)
执行过程(简化,好记)
-
同步阶段:调用栈执行 console.log(1)、console.log(4),输出 1、4;调用栈清空;
-
微任务阶段:执行 Promise.then 回调,输出 3;微任务队列清空;
-
宏任务阶段:执行 setTimeout 回调,输出 2。
✅ 最终输出(直接背):1 → 4 → 3 → 2
🌰 例子 2:Promise 套 Promise(面试官高频坑题)
Promise.resolve().then(() => {
console.log(1)
Promise.resolve().then(() => {
console.log(2)
})
})
Promise.resolve().then(() => {
console.log(3)
})
执行逻辑(易错点重点记)
-
微任务队列遵循“先进先出”原则;
-
微任务里产生的新微任务,会追加到当前微任务队列末尾,不会开启新的微任务轮次。
✅ 最终输出(避免错记成1→2→3):1 → 3 → 2
🌰 例子 3:setTimeout + Promise 混合(进阶题)
setTimeout(() => {
console.log(1)
Promise.resolve().then(() => {
console.log(2)
})
}, 0)
setTimeout(() => {
console.log(3)
}, 0)
执行流程
-
同步代码执行完,两个setTimeout都被委托给浏览器,执行完后进入宏任务队列;
-
调用栈清空,微任务队列为空,执行第一个宏任务:输出1,同时产生微任务;
-
第一个宏任务执行完,调用栈清空,执行新产生的微任务:输出2;
-
微任务清空,执行第二个宏任务:输出3。
✅ 最终输出:1 → 2 → 3
六、async / await 本质(必考,比Promise更常问)
面试一句话答案(直接背)
async / await 是 Promise + Generator 的语法糖,本质还是基于Promise,没有脱离Event Loop机制。
🌰 例子 4:async / await 执行顺序(高频题)
async function foo() {
console.log(1)
await bar()
console.log(2)
}
async function bar() {
console.log(3)
}
foo()
console.log(4)
执行过程拆解(通俗理解)
async函数执行时,遇到await会“暂停”(不是阻塞线程),先执行await后面的函数,然后把await后面的代码,变成Promise.then的微任务。
等价于(帮助理解,不用背):
function foo() {
console.log(1)
// await bar() 等价于 Promise.resolve(bar())
return Promise.resolve(bar()).then(() => {
console.log(2)
})
}
✅ 最终输出(必背):1 → 3 → 4 → 2
关键点(面试必讲):
- await 后面的函数是同步执行的(比如这里的bar(),直接输出3);
- await 后面的代码(console.log(2))会变成微任务;
- await 不会阻塞整个线程,只是“让出执行权”,让同步代码先执行。
七、UI 渲染 & Event Loop(高阶追问,拉开差距)
面试官问:浏览器什么时候渲染页面?
标准回答(直接背)
浏览器的UI渲染,发生在一次Event Loop循环结束后,具体时机是:
-
所有微任务执行完毕之后;
-
下一个宏任务执行之前。
追问:为什么微任务要优先于UI渲染和宏任务?
答:为了保证页面状态更新的一致性。比如微任务里修改了DOM,优先执行完所有微任务,能确保DOM状态稳定后再渲染,避免页面渲染中途被打断,出现“闪屏”或“状态错乱”。
八、requestAnimationFrame 在哪执行?(冷门但高阶)
很多面试官会追问这个,区分你是否真的理解Event Loop细节:
requestAnimationFrame(() => {
console.log('raf')
})
执行位置(必记)
既不是宏任务,也不是微任务,而是介于微任务和UI渲染之间,每一帧(约16.7ms)执行一次,是浏览器专门为动画设计的API,能保证动画流畅不卡顿。
九、面试常见“送命题”汇总(必背,避免踩坑)
这些题看似简单,实则容易答不完整,按下面的标准答案来,不丢分:
Q1:Promise 是宏任务还是微任务?
👉 标准答案:Promise本身是同步任务,只有Promise.then / catch / finally 这些回调函数,才是微任务。(易错点:不要说“Promise是微任务”)
Q2:微任务会不会饿死宏任务?
👉 标准答案:会。如果有无限递归的微任务(比如在Promise.then里再创建新的Promise.then,无限循环),会导致微任务队列永远清空不了,宏任务就永远没有执行的机会,也就是“饿死”。
Q3:setTimeout 0ms 一定立刻执行吗?
👉 标准答案:不一定。原因有两个:① 浏览器对setTimeout有最小延迟限制(通常是4ms),即使设为0,也会至少等待4ms;② 如果主线程(调用栈)正在执行同步代码,setTimeout的回调会一直等待调用栈清空,才能被调度执行。
Q4:为什么JS执行时间太长,会导致页面卡顿?
👉 标准答案:JS执行和页面UI渲染,运行在浏览器的同一个渲染进程中,共享一个主线程。如果JS执行时间太长(比如长时间循环),会阻塞主线程,导致UI渲染无法进行,页面就会出现卡顿、无响应的情况。
十、面试版总结(直接背,结尾升华用)
JS 是单线程语言,通过 Event Loop(事件循环)协调同步和异步任务的执行; 同步代码优先执行,执行完毕后清空调用栈; 微任务在一次事件循环中会被全部清空,哪怕中途产生新的微任务; 宏任务一次只执行一个,执行完后再次回到微任务阶段循环; UI 渲染发生在微任务之后、宏任务之前,保证页面状态一致。