谨记
JavaScript 在浏览器中是单线程执行的。
但需要注意:
JavaScript 本身是单线程执行的,但浏览器运行环境是多线程的。 异步任务由浏览器的其他线程处理,完成后通过 事件循环(Event Loop) 回调到 JS 主线程执行。 如果需要真正的并行计算,可以通过 Web Worker 创建新的 JS 线程。
浏览器事件循环是什么
JavaScript 在浏览器中是 单线程执行的,但浏览器需要处理很多异步任务,例如:网络请求、定时器、用户交互事件、DOM 事件。
为了在不阻塞主线程的情况下处理这些异步任务,浏览器引入了 事件循环(Event Loop)机制。
🌟 核心流程:
- 同步代码进入调用栈
Call Stack执行 - 异步任务交给浏览器的
Web APIs处理(如定时器、网络请求) - 异步任务完成后,其回调函数进入 任务队列
Task Queue - Event Loop 持续检测调用栈是否为空 如果为空,则把任务队列中的任务放入调用栈执行
为什么需要事件循环
由于 JavaScript 是单线程的:同一时间只能执行一段代码,如果没有事件循环,setTimeout就需要等待对应的time才能继续执行,而这会导致:页面卡死。
因此浏览器设计成 = 单线程 JS + 事件循环调度 + 浏览器异步线程。从而实现:非阻塞异步
浏览器运行 JS 的基本结构
- 同步代码进入 调用栈
Call Stack执行 - 异步任务交给浏览器的 Web APIs(例如定时器、网络请求)
- 异步任务完成后,其回调函数会进入 任务队列
Task Queue - Event Loop 会不断检查 调用栈是否为空,如果为空,就把任务队列中的任务放入调用栈执行。
任务队列:宏任务 与 微任务
任务队列中的任务分为两类:Macro Task(宏任务)和 Micro Task(微任务)
宏任务(Macro Task)
定义: 宏任务是由浏览器调度的任务,每次事件循环会执行 一个宏任务。
常见宏任务:
- 整个 JS 文件(script)
- setTimeout
- setInterval
- DOM 事件
- postMessage
- MessageChannel
微任务(Micro Task)
微任务会在 当前宏任务执行结束后立即执行。
常见微任务:
- Promise的回调[.then、.catch、.finally]
- queueMicrotask
- MutationObserver
- await 后面的代码会进入微任务队列
浏览器事件循环的执行顺序是
🔥 执行一个宏任务 → 执行所有微任务 → 浏览器进行一次渲染 → 再执行下一个宏任务
也就是说:微任务的优先级高于宏任务
浏览器渲染时机
浏览器通常会在 宏任务执行完并清空微任务队列后 才进行页面渲染,所以如果微任务不断产生,可能会导致页面渲染被延迟。
常见误区
误区一:同步任务是宏任务,异步任务是微任务 ❌
浏览器调度任务分为宏任务和微任务,从浏览器层面而言,没有同步任务和异步任务的说法。在浏览器层面,同步和异步只是执行方式。同步代码只是 当前宏任务中的执行内容。
误区二:Promise 本身是微任务 ❌ / Promise.then是微任务 ❌
Promise 本身不是微任务。Promise executor 是同步执行,Promise的then、catch、finally的回调才是微任务。
误区三:async是微任务 ❌ / await 是微任务 ❌
这个误区其实和 “Promise 是微任务” 的误区本质类似。 async 本身并不是微任务。async 只是 JavaScript 的一个语法糖,它会让 函数返回一个 Promise。 await 也是语法糖,await = 暂停当前 async 函数 + 注册一个 Promise.then。await的后面的代码才是微任务。
误区四:浏览器执行 JS 时,并不是每一行代码都是一个任务。 ❌
整个JS代码块是一个宏任务。
实战
async function async1() {
console.log(1);
await async2();
console.log(2);
}
async function async2() {
console.log(3);
await Promise.resolve();
console.log(4);
}
async1();
console.log(5);
最后输出: 1 3 5 4 2
拆解:
- 进入 script(一个宏任务)执行 async1()
- 进入 async1()
- 执行 console.log(1); 此时输出 1。
- 执行 await async2();
- 进入 async2()
- 执行 console.log(3); 此时输出 3。
- 执行 await Promise.resolve(); 由于 await = 暂停当前 async 函数 + 注册一个 Promise.then。也就是说 console.log(4); 会被注册成一个微任务。
- 此时 async2() 被暂停,后续代码 console.log(4); 已经进入 微任务队列。
- async2() 返回一个 pending 的 Promise。
- 回到 async1() 中的 await async2(); 由于 async2() 返回的 Promise 还没有 resolve,所以 async1() 也会 被暂停。
- async1() 后面的 console.log(2); 暂时不会执行,也不会进入微任务队列。
- async1() 执行结束(暂停状态),回到 script。
- 执行 console.log(5); 此时输出 5。
- 当前 script(宏任务)执行完毕。
- Event Loop 开始执行 微任务队列。
- 执行微任务 console.log(4); 此时输出 4。
- async2() 执行完成,其返回的 Promise resolve。
- async1() 中的 await async2(); 得到结果,于是继续执行 async1() 剩余代码。
- console.log(2); 被加入 微任务队列。
- 执行该微任务 console.log(2); 此时输出 2。