浏览器 JS执行机制(面试版·可直接背诵)

0 阅读8分钟

一、JS 执行机制一句话总览(先背,面试开篇直接用)

JS 是单线程的,但浏览器不是;JS 负责“执行”,浏览器负责“调度”。

核心支撑就是:Event Loop(事件循环) —— 简单说,就是浏览器帮JS协调“同步任务”和“异步任务”的执行顺序,避免单线程卡死。

二、为什么 JS 是单线程?(面试官高频开篇题)

面试官问:为什么 JS 设计成单线程?

标准回答思路(直接背,不啰嗦)

  • JS 的核心用途是操作 DOM(比如增删改页面元素);

  • 如果设计成多线程,多个线程同时修改同一个 DOM,会出现竞态条件(数据/页面状态不一致)

  • 所以 JS 在执行层面被设计成单线程,避免DOM操作冲突。

关键区分(必记):

  1. JS 是单线程(只有一个执行主线程);
  2. 浏览器是多线程(有专门处理异步、渲染的线程);
  3. 异步 ≠ 多线程(异步是“委托浏览器执行,执行完通知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 的真实执行规则(核心中的核心,必背)

标准顺序(记死,面试直接说,比通俗解释更加分)

  1. 执行 Call Stack(调用栈)中的所有同步代码,直到调用栈清空;
  2. 调用栈清空后,执行当前所有微任务(直到微任务队列清空,哪怕微任务里又产生新微任务,也一并执行);
  3. 微任务队列清空后,执行一次UI渲染(如果浏览器需要渲染页面,比如DOM有变化);
  4. 渲染完成后,从宏任务队列中取出一个宏任务,放入调用栈执行;
  5. 回到第3步,循环往复(这就是“事件循环”)。

一句话简化版(方便快速回忆):

同步 → 微任务 → 渲染 → 宏任务(循环)

五、经典面试例子(逐行拆解,理解后直接背输出顺序)

面试常考“输出顺序题”,掌握这3个例子,同类题都能搞定,每道题重点记“执行逻辑”和“最终输出”。

🌰 例子 1:同步 + Promise + setTimeout(基础必考题)

console.log(1)

setTimeout(() => {
  console.log(2)
}, 0)

Promise.resolve().then(() => {
  console.log(3)
})

console.log(4)

执行过程(简化,好记)

  1. 同步阶段:调用栈执行 console.log(1)、console.log(4),输出 1、4;调用栈清空;

  2. 微任务阶段:执行 Promise.then 回调,输出 3;微任务队列清空;

  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)

执行流程

  1. 同步代码执行完,两个setTimeout都被委托给浏览器,执行完后进入宏任务队列;

  2. 调用栈清空,微任务队列为空,执行第一个宏任务:输出1,同时产生微任务;

  3. 第一个宏任务执行完,调用栈清空,执行新产生的微任务:输出2;

  4. 微任务清空,执行第二个宏任务:输出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

关键点(面试必讲):

  1. await 后面的函数是同步执行的(比如这里的bar(),直接输出3);
  2. await 后面的代码(console.log(2))会变成微任务;
  3. 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 渲染发生在微任务之后、宏任务之前,保证页面状态一致。