异步产生的原因
JavaScript是单线程的,同一时间只能执行一段JavaScript代码,会按顺序进入调用栈执行,一段代码一旦开始执行,就一定会执行到调用栈清空,中途不会被别的异步回调打断(这叫run-to-completion)。需要异步的原因是有很多事情不能立刻完成,如果有耗时任务例如:
定时器:等 1 秒后执行
网络请求:等服务器返回
用户点击:不知道用户什么时候点
图片加载:不知道资源什么时候加载完
文件读取:需要等待 IO
这些任务都让JS主线程一直等,就会卡死,所以就有了异步模型。
异步的意思不是“同时执行JS”,而是:遇到耗时任务时,JS 主线程先不等,把任务交给宿主环境(浏览器/node.js)处理。等任务完成后,再把对应回调放入队列,等待主线程空闲时执行。
异步能力主要由浏览器 / Node.js 这些宿主环境提供。
异步代码
异步代码通常具备一个特点:这段代码不会立刻执行最终回调,而是等某个条件满足后,再把回调交给事件循环调度。
例1:
setTimeout(() => {
console.log('timeout')
}, 1000)执行的过程是:
JS 执行 setTimeout
浏览器开始计时
JS 主线程继续执行后面的代码
1 秒后,浏览器把回调放入任务队列
等主线程空闲后,事件循环取出这个回调执行
例2:
button.addEventListener('click', () => {
console.log('clicked')
})JS 给 button 注册点击事件
用户以后某个时间点击按钮
浏览器捕获点击事件
点击回调进入任务队列
主线程空闲后执行回调
异步分宏任务和微任务
之所以要分宏任务和微任务,就是因为不同异步回调的优先级不同。浏览器不能把所有异步回调都放在同一个队列里,否则 Promise、DOM 事件、定时器、渲染之间的顺序会很混乱。
事件循环里有两个非常重要的队列:
宏任务队列 Macro Task Queue
微任务队列 Microtask Queue
当前同步代码执行完后,先清空所有微任务,再执行下一个宏任务。
宏任务
宏任务可以理解为:一次事件循环中被取出来执行的一个“大任务”。
常见的宏任务包括:
script整体代码:整个文件执行时,可以理解为浏览器先把它作为一个宏任务执行,执行整个 script,先执行 script 这个宏任务中的所有同步代码setTimeoutsetIntervalDOM事件回调MessageChannel:MessageChannel常被用来做任务调度。I/O回调XMLHttpRequest回调setImmediate,主要在 Node.js 中
例1:I/O 回调
浏览器里可以理解为网络、文件、资源加载等完成后的回调。资源加载完成后,回调进入任务队列等待执行
const img = new Image()
img.onload = () => {
console.log('image loaded')
}
img.src = './demo.png'例2:MessageChannel,message 回调通常作为宏任务调度,常用于任务切片、调度器等场景
const channel = new MessageChannel()
channel.port1.onmessage = () => {
console.log('message')
}
channel.port2.postMessage('hello')微任务
微任务可以理解为:当前宏任务结束后、下一个宏任务开始前,必须尽快执行完的小任务。
核心规则是:一个宏任务执行完后,会立即清空当前所有微任务。
常见的微任务:
Promise.then/catch/finally:Promise 本身不是微任务,Promise.then/catch/finally里的回调才是微任务。queueMicrotaskawait 后面的代码MutationObserverprocess.nextTick,Node.js 中更特殊
例1:
console.log(1)
new Promise((resolve) => {
console.log(2)
resolve()
}).then(() => {
console.log(3)
})
console.log(4)
// 输出
// 1
// 2
// 4
// 3
// new Promise 的 executor 是同步执行的,then回调才是微任务例2:queueMicrotask 的作用就是显式创建一个微任务,他比直接写Promise.then(回调函数)更直接:
console.log(1)
queueMicrotask(() => {
console.log(2)
})
console.log(3)
// 输出
// 1
// 2
// 3例3: await 后面的代码是微任务
async function fn() {
console.log(1)
await 100
console.log(2)
}
fn()
console.log(3)
// 输出
// 1
// 3
// 2可以看成
function fn() {
console.log(1)
return Promise.resolve(100).then(() => {
console.log(2)
})
}
fn()
console.log(3)例4:MutationObserver 是微任务,DOM 发生变化后,MutationObserver 的回调会作为微任务执行。
const observer = new MutationObserver(() => {
console.log('mutation')
})
observer.observe(document.body, {
childList: true
})
document.body.appendChild(document.createElement('div'))
console.log('sync')
Node 官方文档说明:
process.nextTick()的回调会在当前操作结束后、当前事件循环迭代内执行,并且会先于setTimeout和setImmediate;Node 文档还明确区分了process.nextTick queue同步代码
↓
process.nextTick
↓
Promise.then / queueMicrotask
↓
setTimeout / setImmediate 等宏任务
事件循环
1. 先执行一个宏任务
例如:整个 script、一次 setTimeout 回调、一次 click 回调
2. 执行这个宏任务里的所有同步代码
3. 如果遇到异步 API,就交给宿主环境处理
例如:setTimeout、DOM 事件、网络请求
4. 当前宏任务执行完,调用栈清空
5. 立即清空所有微任务
例如:Promise.then、queueMicrotask、await 后续代码
6. 微任务清空后,浏览器有机会进行渲染,然后进入下一轮事件循环宏任务里面有微任务
每执行完一个宏任务,都要清空一次微任务队列。
setTimeout(() => {
console.log(1)
Promise.resolve().then(() => {
console.log(2)
})
}, 0)
setTimeout(() => {
console.log(3)
}, 0)
//第一个 setTimeout 回调是一个宏任务
//这个宏任务执行时输出 1
//里面产生了一个 Promise 微任务
//当前宏任务结束后,必须先清空微任务
//所以先输出 2
//然后才执行下一个 setTimeout 宏任务
//输出 3微任务中继续产生微任务
微任务执行过程中产生的新微任务,会追加到微任务队列尾部,并在本轮继续执行,直到微任务队列彻底清空。
Promise.resolve().then(() => {
console.log(1)
Promise.resolve().then(() => {
console.log(2)
})
})
Promise.resolve().then(() => {
console.log(3)
})
// 输出
// 1
// 3
// 2流程如下:
初始微任务队列是:[微任务 A:输出 1, 微任务 B:输出 3]
执行微任务 A:输出
1,并新增一个微任务 C。此时队列变成:[微任务 B:输出 3, 微任务 C:输出 2]
console.log(1)
Promise.resolve().then(() => {
console.log(2)
})然后执行B:3
再执行C:2
面试问答
Q1:说一下事件循环
JavaScript 是单线程语言,同一时间只能执行一段代码。浏览器中的 JS 主线程既要执行脚本,也要响应用户事件和更新 UI。如果所有耗时操作都同步等待,页面就会被阻塞,所以浏览器提供了异步能力。
当 JS 遇到 setTimeout、DOM 事件、fetch 这类异步 API 时,不会阻塞主线程,而是交给浏览器宿主环境处理。等异步任务完成后,对应回调会进入任务队列。
任务队列主要分为宏任务队列和微任务队列。常见宏任务有 script、setTimeout、setInterval、DOM 事件、MessageChannel、I/O。常见微任务有 Promise.then、catch、finally、queueMicrotask、MutationObserver,以及 async/await 中 await 后面的代码。
事件循环的流程是:先执行一个宏任务,比如整个 script;执行过程中同步代码立即执行,异步回调进入对应队列;当前宏任务执行完、调用栈清空后,会立即清空所有微任务;微任务清空后,浏览器有机会进行渲染;然后再从宏任务队列取下一个任务执行。


评论
还没有评论
来发表第一条评论吧!