宏任务和微任务
在JavaScript中,任务被分为两大类:
- 宏任务(Macro Tasks)
- 微任务(Micro Tasks)。
这两类任务是异步执行
的,
它们与JavaScript引擎的事件循环(Event Loop)
和任务调度
有关。
理解这一机制对于掌握JavaScript中的异步编程
至关重要。
一、宏任务 (Macro Tasks)
宏任务是指一个个独立的、可以将更多任务添加到事件循环队列中的任务。每次只有一个宏任务会被执行,并且在当前宏任务执行完毕后,引擎会查看是否有微任务需要执行。
1.1 常见类型
以下是一些典型的宏任务来源:
- 定时器: setTimeout, setInterval。这些API用来注册在未来某个时间点执行的回调函数,成为独立的宏任务。
- I/O: 网络请求、文件操作或其他异步的I/O操作在完成时通常会生成宏任务。
- UI交互: 用户交互事件如点击、滚动、键盘输入等也会创建宏任务。
- 脚本:
<script>
标签中的代码在加载后会作为一个宏任务执行。 - postMessage API: 当使用window.postMessage时,接收消息的部分会形成一个宏任务。
- MessageChannel API: 通过MessageChannel的端点发送消息会导致消息接收者获得一个宏任务。
- setImmediate: 这是Node.js中的一个特殊情况,setImmediate会创建一个宏任务,但它的优先级略低于其他宏任务(注意:这个API不是标准JavaScript的一部分,且在浏览器中通常不可用)。
1.2 特性与行为
独立性
: 每个宏任务都会独立于其它宏任务运行,一个宏任务的执行不会阻塞另外一个宏任务的排队。顺序执行
: 在主线程上,同一时间只会执行一个宏任务,它们会按照排队的顺序依次执行。界限清晰
: 宏任务之间有明确的边界,一个宏任务完成后,浏览器可以选择进行UI渲染,然后再开始下一个宏任务。- 能
产生
新的微任务
: 在宏任务执行过程中可能会创建新的微任务,这些微任务会被添加到当前事件循环的微任务队列
中,在当前宏任务结束后统一执行
。
1.3宏任务和事件循环
当事件循环开始一个新的迭代时,会从宏任务队列中取出一个任务进行处理。
如果在处理宏任务的过程中生成了微任务(例如,一个Promise被解决),这些微任务会添加到微任务队列,并且在当前宏任务完成后立即执行。
这意味着微任务有机会在下一个宏任务开始前改变系统的状态。
一旦当前宏任务和随后的所有微任务执行完毕,如果在浏览器环境中,浏览器可能会更新渲染(re-render)页面,反映出任何由于之前的任务执行而发生的更改。
之后,事件循环会移动到下一个宏任务,并继续这个循环。
对于开发者来说,理解宏任务对于管理JavaScript中的异步操作
以及提供流畅的用户体验是非常重要的。
例如,过多的宏任务会阻塞UI渲染,
导致应用程序响应缓慢,
因此合理安排宏任务的执行是很关键的。
二、微任务 (Micro Tasks)
微任务(Micro Tasks)是JavaScript中的一种异步任务类型,相比于宏任务(Macro Tasks),它们具有更高的优先级。
当执行栈为空时,在下一个宏任务被处理之前
,所有的微任务都会被依次执行。这意味着微任务可以用来安排一些需要尽快在当前脚本运行环境结束后立即执行的操作。
2.1 特性与行为
- 高优先级: 微任务始终在下一个宏任务开始前执行完毕。
- 批量执行: 在当前宏任务执行完毕之后,所有排队的微任务会一次性执行完毕。
- 连锁反应: 微任务的执行可以导致更多微任务被生成,并且新生成的微任务也会在当前事件循环迭代中执行。
- 无阻塞: 微任务的执行不会阻塞UI渲染,它们在宏任务和UI渲染之间完成。
2.2 常见类型
以下是几个产生微任务的典型场景:
- Promise回调: Promise对象的
then
,catch
, 和finally
方法注册的回调函数都会放入微任务队列。 - MutationObserver API: 当DOM发生变化时,
MutationObserver
注册的回调是作为微任务执行的。 process.nextTick
: 这是Node.js特有的API,它允许指定一个回调函数在当前操作结束或者当前宏任务结束后、下一轮事件循环之前的微任务阶段调用。queueMicrotask
函数: ES2019中引入了queueMicrotask
方法,允许开发者直接将回调函数加入到微任务队列。
微任务在设计上用来处理高优先级的任务,如响应动作的边缘情况,或者在当前执行栈结束之前需要执行的任务。因此,微任务经常用于确保在运行某些代码之前能够完成必要的更新。
2.3 微任务和事件循环
微任务与事件循环的关系非常紧密。在每次事件循环的一个迭代(tick)中,引擎首先会选择并执行一个宏任务(如果有的话),然后执行所有的微任务。这里的关键点是,只要微任务队列不为空,就会一直执行微任务直至清空,即使在执行微任务的过程中又有新的微任务被添加进来。
因此,如果微任务连续生成其他微任务,可能会导致延迟后续宏任务的执行,因为事件循环会被“占据”在微任务的执行上。
从实践角度出发,这意味着微任务适合用于那些必须要在当前脚本或当前宏任务结束之前立即完成的小工作。但是,需要注意的是,过多的微任务或者无限循环的微任务产生,可能导致长时间阻塞主线程,影响用户体验。
2.4 使用场景
微任务经常用于确保一段代码在当前执行栈执行完毕后尽快运行
,同时又不想等到下一个事件循环迭代。
例如,你可能希望在数据变化后立即对数据做一些处理,而不是等待其他宏任务。这时候微任务就非常有用,因为它们能够在当前宏任务后和任何其他宏任务(包括事件处理器、定时器回调等)之前执行。
正确地使用微任务,可以有效地利用JavaScript单线程的异步特性,提升性能和用户体验。
三、事件循环和任务执行顺序
事件循环(Event Loop)是JavaScript运行时的一个核心概念,它负责协调代码执行、事件分发、定时器触发和其他异步功能。
虽然JavaScript是单线程的,但事件循环机制使得它可以执行非阻塞操作,允许我们编写看似多线程的并发代码。
3.1 事件循环的基本工作原理
JavaScript引擎使用事件循环来协调这些任务的执行。事件循环的每个循环迭代称为一个“tick”。
事件循环的工作原理可以描述为一个重复的循环过程,它遵循以下步骤:
-
取出任务: 首先从宏任务队列中取出队列最前面的任务(如果有的话)。
-
执行任务: 执行这个宏任务,期间可能会有新的微任务被创建。
-
执行微任务: 宏任务执行完成后,事件循环会处理所有的微任务。只要微任务队列不为空,就继续一一取出并执行微任务。
-
渲染更新: 如果在浏览器环境中,且此时到了渲染的时机,则进行页面渲染。
-
检查是否还有宏任务: 如果宏任务队列中还有任务,返回第一步;否则,等待直至新的宏任务被添加到队列中。
循环上述过程.
3.2任务执行顺序
在了解了事件循环的工作原理后,我们可以更清楚地理解任务的执行顺序:
-
当JavaScript引擎开始执行脚本时,它首先会处理全部同步代码。
-
当同步代码执行完毕,引擎开始检查宏任务队列。如果有待处理的宏任务,它将执行其中的一个。
-
在一个宏任务执行结束之后,引擎会查看微任务队列并执行所有微任务。在执行微任务的过程中,新产生的微任务也会被加入当前的微任务队列,并在当前循环迭代中执行。
-
一旦微任务队列为空,如果浏览器需要进行UI渲染,那么它会处理渲染步骤。
-
完成渲染后,如果宏任务队列仍然有任务,事件循环会开始新一轮的迭代;否则,它将等待直到新的宏任务到达队列。
注意点
- 微任务总是
在下一个宏任务执行前完成
,所以它们通常用于确保某些操作在继续下一个宏任务之前完成。 - 由于微任务在当前宏任务后立即执行,所以它们可能会延迟后续宏任务的执行。
- 如果微任务队列因产生大量微任务而无法快速清空,那么可能会影响应用程序的性能,尤其是对于帧率敏感的动画或者响应用户交云的操作。
- 浏览器可能会施加限制,规定在每个渲染帧内只能执行一定量的JavaScript代码,以确保页面的平滑渲染。
四、实例理解
例1
假设你有如下的代码片段:
console.log('Script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('Script end');
执行顺序将会是:
Script start
Script end
promise1
promise2
setTimeout
这里发生了什么?
- ‘Script start’ 是同步代码,首先执行。
- setTimeout 被推迟到下一个宏任务。
- Promise.resolve() 创建一个已解决的Promise,它的then()方法注册的回调被添加到微任务队列。
- ‘Script end’ 同步执行。
- 当前宏任务结束,开始执行微任务队列中的任务,依次打印 ‘promise1’ 和 ‘promise2’。
- 第一个宏任务(setTimeout 的回调)在所有微任务执行完毕后执行,打印 ‘setTimeout’。
通过了解宏任务和微任务的区别,可以更好地预测和理解JavaScript代码的执行顺序,尤其是在复杂的异步编程场景中。
例2
当浏览器加载并执行<script>
标签中的JavaScript代码时,这个过程本身可以被视为一个宏任务。
这意味着当页面开始加载,并且解析到一个<script>
标签时,其中的JavaScript代码会作为一个宏任务加入到事件循环中去执行。
在这个宏任务执行的过程中,可以产生额外的宏任务和微任务
。
例如,如果脚本中有异步操作(如使用setTimeout、setInterval或者发起网络请求等),这些异步操作会创建新的宏任务,它们将被添加到宏任务队列中,等待当前宏任务以及所有已经排队的微任务完成后再执行。
此外,如果脚本中使用了Promise或者其他会产生微任务的API(如queueMicrotask),那么这些操作会生成微任务,这些微任务会被添加到微任务队列中,在当前宏任务结束后立即执行,而在下一个宏任务开始之前完成。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Macro Task Example</title>
</head>
<body>
<script>
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
}).then(() => {
console.log('Promise 2');
});
console.log('Script end');
</script>
</body>
</html>
在上述例子中,整个
- setTimeout函数调用创建了一个宏任务,该任务被添加到宏任务队列中,以便在未来某个时刻执行。
- Promise.resolve()创建了一个微任务,该任务被添加到微任务队列中,以便在当前宏任务执行完毕后立即执行。
最后的执行顺序将是:
Script start
Script end
Promise 1
Promise 2
setTimeout
所以,你可以认为任何一段JavaScript代码的执行都是在宏任务的上下文中进行的,无论是由事件触发的回调、解析