一. 引言
在前端开发中,尤其是在面试中,我们经常听到宏任务(Macro Task)和微任务(Micro Task)这两个概念。这两个概念是在 JavaScript 中任务调度过程中的重要组成部分,对于理解事件循环(Event Loop)机制至关重要。本文将深入解析宏任务与微任务的含义、区别以及实际应用,帮助大家更清晰的理解事件循环机制。
请看以下一个小示例,请说明下面代码执行输出顺序:
console.log("1");
setTimeout(() => {
console.log("2");
Promise.resolve().then(() => console.log("3"));
});
Promise.resolve().then(() => console.log("4"));
console.log("5");
如果你能随口说出来并能说明原因,说明你对事件循环掌握的不错,值得点赞!
二. 宏任务(Macro Task)
宏任务是指由 JavaScript 主线程执行的任务,它包括但不限于以下情况:
-
浏览器事件(如 click、mouseover 等)
-
定时器任务(如 setTimeout 和 setInterval)
-
页面渲染(如 回流或重绘)
-
事件回调(如 I/O、点击事件等)
-
网络请求 (如 XMLHttpRequest 和 fetch 等)
宏任务通常独立于当前任务,并按顺序排队执行。以下是一些常见的代码示例来说明宏任务的概念和用法:
示例 1: 使用事件监听器创建宏任务
// 事件监听器创建宏任务
const button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("Button clicked");
});
console.log("Waiting for button click...");
解释:在这个示例中,等待按钮点击的语句是同步任务,而当按钮被点击时,事件回调函数会作为宏任务被执行。
输出结果为:
示例 2: 使用定时器创建宏任务
// 定时器任务
console.log("Start");
setTimeout(() => {
console.log("In Timeout");
}, 2000);
console.log("End");
// 事件监听器创建宏任务
const button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("Button clicked");
});
console.log("Waiting for button click...");
解释:在这个示例中,打印 "Start" 和 "End" 的语句是同步任务,而通过 setTimeout 创建的回调函数被作为宏任务,在 2000 毫秒后才执行,所以在执行宏任务之前会先输出同步任务的结果。
输出结果为:
示例 3: 页面渲染
console.log("Start");
// 修改页面样式
document.body.style.backgroundColor = "red";
console.log("End");
解释:在这个示例中,修改页面样式是一个宏任务,当样式被修改后,浏览器会执行重新渲染页面的操作。
输出结果为:
示例 4: 使用 XMLHttpRequest 发起网络请求
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data");
xhr.addEventListener("load", () => {
console.log("Request completed");
});
xhr.send();
console.log("Waiting for request to complete...");
解释:在这个示例中,使用 XMLHttpRequest 发起网络请求是一个宏任务。当请求完成后,load 事件回调函数会作为宏任务被执行。
输出结果为:
总结: 宏任务的使用广泛,包括定时器任务、网络请求、事件监听器等。理解宏任务的概念和用法可以帮助我们正确处理 JavaScript 中的异步操作,并合理安排任务的执行顺序,以提高应用的性能和用户体验。
三. 微任务(Micro Task)
微任务是指由 JavaScript 引擎执行的任务,它在宏任务之后执行,但在下一次渲染之前执行。微任务通常是由宏任务中的某个特定任务触发的,并立即执行。常见的微任务有:
-
Promise 的回调函数
-
Async/Await 函数
-
MutationObserver 的回调函数
-
process.nextTick(Node.js 环境下)
示例 1:微任务的执行顺序
还是沿用本文章开头所使用的代码示例,说明微任务的执行顺序:
console.log("1");
setTimeout(() => {
console.log("2");
Promise.resolve().then(() => console.log("3"));
});
Promise.resolve().then(() => console.log("4"));
console.log("5");
解释:
-
在第一个宏任务中,同步的打印语句
1
和5
首先执行。 -
然后,第一个宏任务中使用
setTimeout
创建了一个回调函数,它被添加到宏任务队列中等待执行。 -
在第一个宏任务执行结束前,微任务队列中的回调函数执行。
Promise.resolve().then(() => console.log('4'))
的回调函数首先被添加到微任务队列中,因此会在2
之前执行,打印4
。 -
当第一个宏任务任务队列为空时,开始执行第二个宏任务,打印
2
。 -
然后,Promise 的回调函数
Promise.resolve().then(() => console.log('3'))
会被添加到微任务队列中等待执行。 -
在本轮事件循环中,微任务队列中的任务会按序执行,因此打印
3
。
输出结果为:
结论:
微任务是 JavaScript 中处理异步操作的一种机制,它通过及时响应并在当前任务结束后立即执行,有助于编写更高效和灵活的异步代码。了解微任务的概念和用法能够帮助我们更好地利用异步特性,提升代码的性能和用户体验。
四. 宏任务与微任务的区别
宏任务和微任务主要在两个方面有所区别:执行时机和调度机制
1. 执行时机
-
宏任务:宏任务是由 JavaScript 引擎在执行栈(执行同步任务)和任务队列中的任务之间切换时执行的。宏任务在下一个宏任务之前执行,并按照宏任务队列的顺序执行。
-
微任务:微任务是在宏任务执行结束,下一个宏任务开始之前执行的任务。微任务在当前宏任务中执行完后立即执行,并按照微任务队列的顺序执行。
宏任务在主线程上执行,而微任务在宏任务执行完毕之后执行,即在下一轮事件循环的步骤中执行,这也是为什么微任务会在宏任务之前执行的原因。
2. 调度机制
-
宏任务:宏任务由 JavaScript 引擎的任务调度器调度执行。当主线程执行完当前宏任务后,会检查是否存在微任务,如果存在,则会执行所有微任务,然后选择下一个宏任务执行。
-
微任务:微任务同样由 JavaScript 引擎的任务调度器调度执行。当微任务队列中存在微任务时,会依次执行微任务,直到微任务队列为空。
宏任务使用先进先出的调度机制,即它们按照任务的顺序排列,并按顺序执行。
而微任务则使用一个任务队列(microtask queue)进行调度,当某个宏任务执行完毕后,会立即将所有的微任务添加到任务队列中,并按照先进先出的顺序依次执行。
宏任务和微任务的区别在于它们的执行机制和调度机制。宏任务在下一个宏任务执行之前执行,而微任务在当前宏任务结束后立即执行。微任务优先级高于宏任务,因此在同一轮事件循环中,微任务会优先执行。了解宏任务和微任务的区别对于编写高效的异步 JavaScript 代码非常重要。
仅凭文字描述要理解这两个机制并不容易,因此通过下面的事件循环机制的的说明消化一下这两个机制。
五. 事件循环机制(Event Loop)
事件循环(Event Loop)机制是 JavaScript 引擎用来处理异步任务的一种机制。它负责维护一个任务队列,并按照一定的规则循环执行任务队列中的任务。
在事件循环机制中,任务分为宏任务和微任务:
如上图,当同步任务执行完毕后,就会执行所有的宏任务,宏任务执行完成后,会判断是否有可执行的微任务;如果有,则执行微任务,完成后,执行宏任务;如果没有,则执行新的宏任务,形成事件循环。
事件循环机制的整体执行流程如下:
-
执行同步任务:JavaScript 代码从上到下逐行执行同步任务,直到遇到第一个异步任务。
-
处理微任务:请注意,当遇到一个微任务时,将其添加到微任务队列中,而不是立即执行。
-
执行宏任务:当同步任务执行完毕或遇到第一个微任务时,执行宏任务队列中的第一个任务。执行宏任务时,如果遇到嵌套的微任务,也会将其添加到微任务队列中等待执行。
-
执行微任务:执行完一个宏任务后,立即处理微任务队列中的所有任务,按照顺序依次执行。
-
重复上述步骤:不断地循环执行上述步骤,直到任务队列为空。
需要注意的是:微任务比宏任务优先级要高。
在同一个任务中,如果既有微任务又有宏任务,那么微任务会先执行完毕。
在不同的任务中,宏任务的执行优先级要高于微任务,因此在一个宏任务执行完毕后,它才会执行下一个宏任务和微任务队列中的任务。
六. 总结
宏任务和微任务是 JavaScript 中任务调度过程的重要概念,理解它们的含义、特点和区别有助于我们更好地掌握事件循环机制,编写出高效的 JavaScript 代码。
在实际开发中,合理地使用宏任务和微任务可以提高页面性能,同时可以减少 bug 的产生。
希望本篇文章的解析能够帮助大家更清晰地理解和应用宏任务和微任务,以及事件循环机制。