切换主题
字数
4573 字
阅读时间
18 分钟
https://juejin.cn/editor/drafts/7417521775607840794 今天问了chatgpt个问题:"什么是宏任务和微任务"
gpt回答:
宏任务和微任务是 JavaScript 中的两种任务调度机制,它们在事件循环(Event Loop)中执行的顺序不同。理解它们的区别有助于更好地掌握 JavaScript 的异步编程。
宏任务(Macro Task)
宏任务是较大粒度的任务,通常包括:
setTimeout
setInterval
I/O 操作(如网络请求、文件读取)
UI 渲染
每次事件循环的开始,都会执行一个宏任务队列中的任务。
微任务(Micro Task)
微任务是较小粒度的任务,主要用于处理一些需要立即执行的任务,通常包括:
Promise
的回调(.then
、.catch
)MutationObserver
queueMicrotask
微任务在当前宏任务执行完成后,且在下一个宏任务开始之前执行。
执行顺序
- 执行一个宏任务。
- 执行所有微任务,直到微任务队列为空。
- 渲染更新(如果需要)。
- 执行下一个宏任务。
读到 微任务在当前宏任务执行完成后,且在下一个宏任务开始之前执行。
感觉很奇怪,之前看到的八股文写的都是 微任务 比 宏任务 先执行, 为什么看gpt描述好像不是这个样子, 又去问了Claude"关于浏览器异步机制 是谁定义的,我想找到官网对于 异步执行机制和事件循环的定义"
Claude:
浏览器的异步机制和事件循环主要由两个标准定义:
- ECMAScript 规范
- HTML 规范
让我为您详细解释这两个标准以及它们对异步机制的定义:
特别是在 "Event loops" 章节,你可以找到关于事件循环、宏任务和微任务的详细说明:
https://html.spec.whatwg.org/multipage/webappapis.html#event-loops
这个章节详细描述了事件循环的工作原理,包括:
- 任务队列的管理
- 微任务的执行时机
- 宏任务和微任务的区别
值得注意的是,虽然 "宏任务" 和 "微任务" 这两个术语在开发者社区中广泛使用,但在规范中,它们的正式名称略有不同:
- 宏任务在规范中被称为 "task"
- 微任务在规范中被称为 "microtask"
这两个规范共同定义了 JavaScript 和浏览器环境中的异步行为。ECMAScript 规范主要关注语言层面的特性,而 HTML 规范则定义了这些特性在浏览器环境中如何与事件循环交互。
听从Claude的建议,我将宏任务这个术语忘记,果然马上读懂了微任务,以下黑体字是我认为重点的官方文档段落 MDN 的文档
微任务(Microtasks)
起初微任务和任务之间的差异看起来不大。它们很相似;都由位于某个队列的 JavaScript 代码组成并在合适的时候运行。但是,只有在迭代开始时队列中存在的任务才会被事件循环一个接一个地运行,这和处理微任务队列是殊为不同的。
有两点关键的区别。
首先,每当一个任务存在,事件循环都会检查该任务是否正把控制权交给其他 JavaScript 代码。如若不然,事件循环就会运行微任务队列中的所有微任务。 接下来微任务循环会在事件循环的每次迭代中被处理多次,包括处理完事件和其他回调之后。
其次,如果一个微任务通过调用
queueMicrotask()
, 向队列中加入了更多的微任务,则那些新加入的微任务 会早于下一个任务运行。这是因为事件循环会持续调用微任务直至队列中没有留存的,即使是在有更多微任务持续被加入的情况下。
w3c的HTML标准文档: 8.1.6.1 定义
每个 事件循环 有一个 当前正在执行的任务, 它是一个 任务 或 null,初始为 null。用来处理再进入的情况。
每个 事件循环 有一个 微任务
队列
, 它是一个 微任务 的 队列,初始为空。 微任务 是一个通俗的叫法,指由 入队微任务 算法创建的 任务。
事件循环
在 事件循环 期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。 正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。
函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。
jswhile (queue.waitForMessage()) { queue.processNextMessage(); }
重点:
忘记 宏任务的概念,没有直接对宏任务和微任务谁先执行的定义,一次事件循环中执行一个任务后会清空微任务队列,然后进行下一轮事件循环. ![[Pasted image 20240923225303.png]] 图片来源
事件循环中的"任务队列"实际上不仅包括微任务队列,还包括多个其他类型的宏任务队列。
微任务队列:
- 微任务队列是一个独立的、优先级最高的队列。
- 它包含了Promise、queueMicrotask、MutationObserver等任务。
宏任务队列:
- 所谓的"宏任务队列"实际上是多个不同类型任务队列的集合。
- 它包括setTimeout/setInterval队列、I/O队列、UI渲染队列等。
- 这些队列之间也有一定的优先级顺序。
消息队列和任务队列是一回事
任务队列 不是 队列,是一个集合 ![[Pasted image 20240923225241.png]] 练习题1
js
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);
new Promise((resolve) => {
console.log(4);
resolve();
console.log(5);
}).then(() => {
console.log(6);
});
console.log(7);
执行顺序解析:1 => 3 => 4 => 5 => 7 => 6 => 2。
- 创建Promise实例是同步的,所以1、3、4、5、7是同步执行的。
then
方法是微任务,放入微任务队列中,在当前脚本执行完毕后立即发生。- 同步任务执行完毕后,执行微任务队列中的微任务。
- 最后,
setTimeout
放入宏任务队列,按照先进先出的原则执行。注意:出现
async
、await
,等价于promise
、then
。
练习题2
js
//必考题
console.log("script start");
async function async1() {
// await async2(); //浏览器给await开小灶,await后面的代码当成同步代码立即调用
let x = await new Promise((resolve) => {
console.log("🚀 ~ x ~ x :");
setTimeout(() => {
resolve("settimeout in async1");
}, 2000);
});
console.log(x);
console.log("async1 end");
}
async function async2() {
console.log("async2 end");
}
async1();
setTimeout(function () {
console.log("setTimeout");
}, 0);
new Promise((resolve) => {
console.log("Promise");
resolve();
})
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});
console.log("script end");
- script start
- 🚀 ~ x ~ x :
- Promise
- script end
- promise1
- promise2
- setTimeout
- settimeout in async1
- async1 end
若将第四行代码解注释,则会变成
- script start
- async2 end
- Promise
- script end
- 🚀 ~ x ~ x :
- promise1
- promise2
- setTimeout
- settimeout in async1
- async1 end
await 关键字本身不属于任何任务队列,但它的行为与微任务密切相关。await 的执行涉及到 Promise 的处理,因此它的异步部分实际上是在微任务队列中执行的。
await 的执行过程:
当遇到 await 时,JavaScript 引擎会暂停当前 async 函数的执行。
await 后面的表达式会立即执行。
函数剩余的部分会被包装成一个微任务,等待 await 的结果。
微任务队列的关系:
await 表达式的结果(如果是 Promise)会在微任务队列中解析。
async 函数剩余的部分会作为一个新的微任务添加到队列中。
相关知识
node中的process.nextTick
- process.nextTick的性质: 严格来说,process.nextTick既不是宏任务也不是微任务。它有自己的特殊队列。
- 执行顺序: process.nextTick回调的执行优先级高于微任务。它会在当前操作完成后、IO事件的回调被触发之前执行,甚至优先于Promise等微任务。
- 与事件循环的关系: process.nextTick在事件循环的每个阶段完成后、进入下一个阶段之前执行。
- 使用注意事项: 过度使用process.nextTick可能会导致IO饥饿,因为它会在所有IO操作之前执行。
例子说明process.nextTick的行为:
js
console.log('1. 开始');
setTimeout(() => {
console.log('5. setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('4. Promise');
});
process.nextTick(() => {
console.log('3. nextTick');
});
console.log('2. 结束');
这段代码的输出顺序将是:
- 开始
- 结束
- nextTick
- Promise
- setTimeout
解释:
- 首先执行同步代码,打印"开始"和"结束"。
- process.nextTick回调被添加到nextTick队列。
- Promise.then回调被添加到微任务队列。
- setTimeout回调被添加到定时器队列(宏任务)。
- 同步代码执行完毕后,立即执行nextTick队列中的回调。
- 然后执行微任务队列中的Promise回调。
- 最后执行宏任务队列中的setTimeout回调。
重要说明:
- 这个行为是Node.js特有的。在浏览器环境中不存在process.nextTick。
- 在Node.js中,process.nextTick是实现"异步但尽可能快"的一种方式。
- 虽然process.nextTick不是微任务,但它的行为更接近微任务而非宏任务。
总结: process.nextTick不能简单地归类为宏任务或微任务。它有自己的特殊队列,执行优先级高于常规的微任务和宏任务
Vue.js中的nextTick
Vue.js 中的 nextTick
是一个非常有趣的概念,它与浏览器的事件循环机制有着密切的关系。让我来为您详细解释一下:
在 Vue.js 中, this.$nextTick()
是一个用于在 DOM 更新后执行回调函数的方法。它的工作原理如下:
微任务队列:
- Vue.js 利用微任务队列来处理 DOM 更新。
- 当你更新 Vue 组件的状态时,Vue 会将 DOM 更新操作放入微任务队列。
$nextTick() :
this.$nextTick()
会将回调函数放入微任务队列,在下一个 DOM 更新周期执行。- 这意味着,当你调用
this.$nextTick(callback)
时,callback 函数会在 DOM 更新完成后执行。
事件循环:
- 浏览器的事件循环会先执行所有同步代码,然后依次执行微任务队列和宏任务队列。
- 当同步代码修改了 Vue 组件的状态时,Vue 会将 DOM 更新操作放入微任务队列。
- 之后事件循环会执行这些微任务,从而完成 DOM 的更新。
所以,this.$nextTick()
实际上是利用了浏览器事件循环的微任务队列机制。它确保回调函数在下一个 DOM 更新周期执行,这对于某些需要操作更新后 DOM 的场景非常有用。
下面是一个简单的例子来演示 $nextTick()
的用法:
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
function incrementCount() {
count.value++
this.$nextTick(() => {
// 这个回调函数会在 DOM 更新后执行
console.log('DOM 已更新, count 的值为:', count.value)
})
}
</script>
<template>
<button @click="incrementCount">
点击增加计数: {{ count }}
</button>
</template>
在这个例子中,当用户点击按钮时,incrementCount()
函数会先更新 count
的值,然后在 $nextTick()
的回调函数中访问已更新的 DOM。这确保了回调函数能够操作最新的 DOM 状态。
总之,Vue.js 中的 $nextTick()
利用了浏览器事件循环的微任务队列机制,确保回调函数在下一个 DOM 更新周期执行。这在某些需要操作更新后 DOM 的场景下非常有用。
为什么要拆分成多个任务队列
在JavaScript中,微任务会优先于宏任务执行, 这意味着,在当前任务执行结束后,所有微任务都会被立即执行,而宏任务只有在所有微任务执行完毕后才会执行, 这种执行顺序保证了微任务的优先级,可以避免一些问题的出现。
1、 比如,处理 Promise对象时可能会出现的竞态条件,
举个例子,当我们使用Promise对象时,它会返回一个Promise实例并将回调函数放入微任务队列中,
当 Promise的状态发生改变时,它会立即执行微任务队列中的回调函数,而不是等待当前任务结束后再执行,
这种特性可以保证 Promise回调函数的执行顺序,避免出现竞态条件,从而使代码更加可靠。
补充(举例解释一下): 假设,有两个Promise对象P1、P2, 它们的状态都发生了改变,但是P1的回调函数在微任务队列中先于P2的回调函数执行, 这种情况下,即使P2的状态改变发生在P1之前,P2的回调函数也会等待P1的回调函数执行完毕后再执行, 这样可以避免多个回调函数同时执行而产生的竞态条件。
如果当前任务执行一半了,Promise状态发生改变了,会停下当前任务去执行微任务队列中Promise的回调函数吗?????? 不会的, 即使当前任务执行了一半,如果Promise状态发生改变,也会立即执行微任务队列中的回调函数, 但是不会停下当前任务的执行。当前任务的执行会继续完成,然后才会回到微任务队列中执行其他的回调函数, 因此,即使Promise状态发生改变,也不会直接打断当前任务的执行。
2、 另一方面,宏任务的执行是在当前任务结束后才会执行的,这意味着,可以将一些耗时的操作放入宏任务队列中,从而避免阻塞当前任务的执行,
比如,我们可以将一些需要等待一段时间才能执行的代码放入 setTimeout 的回调函数中,
这样可以使页面在执行这些代码的同时仍然保持响应,提高用户体验。
因此,JavaScript 之所以要区分微任务和宏任务,是为了保证异步操作的正确性和性能。
JS中微任务和宏任务执行顺序 1、首先执行当前代码(同步任务),直到遇到第一个宏任务或微任务,
2、如果遇到微任务,则将它添加到微任务队列中,继续执行同步任务,
3、如果遇到宏任务,则将它添加到宏任务队列中,继续执行同步任务,
4、当前任务执行完毕后,JavaScript引擎会先执行所有微任务队列中的任务,直到微任务队列为空,
5、然后执行宏任务队列中的第一个任务,直到宏任务队列为空,
重复步骤4、步骤5,直到所有任务都被执行完毕。
需要注意的是,微任务比宏任务优先级要高,因此在同一个任务中,如果既有微任务又有宏任务,那么微任务会先执行完毕,
而在不同的任务中,宏任务的执行优先级要高于微任务,因此在一个宏任务执行完毕后,它才会执行下一个宏任务和微任务队列中的任。
举个例子, 假设当前代码中有一个 setTimeout(宏任务) 和一个 Promise(微任务),它们分别对应一个宏任务和一个微任务。那么执行顺序如下:
1、执行当前代码,将 setTimeout 和 Promise 添加到宏任务和微任务队列中,
2、当前任务执行完毕,JavaScript引擎,先执行微任务队列中的 Promise回调函数,
3、微任务队列为空后,再执行宏任务队列中的 setTimeout回调函数。
需要注意的是:在一些特殊情况下,微任务和宏任务的执行顺序可能会发生变化,比如在使用 MutationObserver 监听 DOM 变化时, 它会被视为一个微任务,但是它的执行顺序可能会比其他微任务更靠后。因此,需要根据具体情况来理解和处理微任务和宏任务的执行顺序。
原文链接:https://blog.csdn.net/pig_ning/article/details/134437019
贡献者
sunchengzhi