在JavaScript的异步编程模型中,宏任务(MacroTask)和微任务(MicroTask)是两个核心概念。它们共同构成了JavaScript的事件循环(Event Loop),使得JavaScript能够非阻塞地执行异步操作。了解宏任务和微任务的工作原理,对于编写高效、可维护的异步代码至关重要。
一、宏任务(MacroTask)
宏任务是由宿主环境(例如浏览器或Node.js)提供的任务,通常包括:
- 整体代码执行(script) :在浏览器加载页面时,首先会执行整体的JavaScript代码。这个执行过程本身就是一个宏任务。
- setTimeout 和 setInterval:这两个是JavaScript中常用的定时器函数,用于在指定的时间后执行某个函数,或者每隔一段时间执行某个函数。当定时器时间到达时,会触发一个宏任务,将回调函数放入宏任务队列中等待执行。
- setImmediate(Node.js特有) :在Node.js环境中,setImmediate()函数用于在I/O事件完成后,但在其他宏任务(如setTimeout)之前执行回调函数。因此,它也可以被视为一个宏任务。
- I/O 操作:输入/输出操作,如读取文件、网络请求等,通常也是宏任务。当I/O操作完成时,会触发相应的回调函数,并将这些回调函数作为宏任务放入任务队列中。
- UI渲染:在浏览器中,当页面需要重绘或重排时,也会触发宏任务。这些任务通常与页面的渲染性能相关。
- 事件回调:包括DOM事件(如点击、滚动等)和Web API事件(如XMLHttpRequest完成等)。当这些事件发生时,会触发相应的回调函数,并将这些回调函数作为宏任务放入任务队列中。
当JavaScript引擎开始执行代码时,会首先执行同步代码,然后将异步代码(如setTimeout、setInterval等)放入宏任务队列中等待执行。
二、微任务(MicroTask)
微任务是由JavaScript引擎自己维护的任务队列:
- Promise 的回调:在 JavaScript 中,Promise 用于处理异步操作。当 Promise 的状态从 pending 变为 resolved 或 rejected 时,会执行相应的回调函数,这些回调函数就可以被视为微任务。
- async/await:这是基于 Promise 的语法糖,用于简化异步代码的书写。当使用 async/await 编写的异步函数执行完毕后,其后续的代码(包括 then 或 catch 中的回调函数)也会作为微任务执行。
- MutationObserver:在浏览器中,MutationObserver 用于监听 DOM 树的变化。当监听到变化时,会触发回调函数,这些回调函数也是微任务。
- process.nextTick(Node.js 独有):在 Node.js 中,process.nextTick() 方法用于将回调函数放在当前执行栈的末尾,即在当前同步任务执行完毕后立即执行,它的优先级比 Promise 的回调要高。
- Object.observe(已废弃):虽然 Object.observe 方法已被废弃,并被 Proxy 对象替代,但在它存在的时候,用于监听对象属性的变化,并在变化时触发回调函数,这些回调函数也是微任务。
当JavaScript引擎执行完一个宏任务后,会检查微任务队列是否有待执行的微任务,如果有,则清空微任务队列中的所有任务,然后再执行下一个宏任务。
三、宏任务与微任务的执行顺序
JavaScript引擎在执行异步代码时,会按照以下顺序进行处理:
- 执行一个宏任务(通常是整体代码)。
- 执行完宏任务后,检查并执行所有微任务。
- 重复上述步骤,直到宏任务队列和微任务队列都为空。
四、示例
下面是一个简单的示例,演示了宏任务
和微任务
的执行顺序:
例子1
思考下,以下会输出什么
javascript
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'); // 宏任务
输出结果:
arduino
script start
script end
promise1
promise2
setTimeout
是否跟你想的一致呢?接下来我们解析一下吧,其实聪明的你看到注释应该已经明白一切了。
- 首先执行同步代码,输出"script start"。
- 然后遇到setTimeout,将其回调函数放入宏任务队列。
- 接着遇到Promise的then方法,将其回调函数放入微任务队列。
- 继续执行同步代码,输出"script end"。
- 此时,一个宏任务执行完毕,JavaScript引擎开始执行微任务队列中的所有任务,输出"promise1"和"promise2"。
- 微任务队列为空后,执行下一个宏任务(setTimeout的回调函数),输出"setTimeout"。
例子2
这次没注释了喔,再来猜猜看会输出什么
javascript
console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
这次的你是否跟答案想的一致呢,如果一致那么恭喜你,你已经掌握区分宏任务微任务的能力了,接下来我们剖析下代码的执行顺序。
- 首先执行同步代码,输出"script start"。
- 然后遇到Promise,执行里面的回调,输出"promise1";"promise1 end"。
- 接着遇到Promise的then方法,将其回调函数放入微任务队列。
- 继续执行同步代码,然后遇到setTimeout,将其回调函数放入宏任务队列。
- 继续执行同步代码,输出"script end"。
- 此时,一个宏任务执行完毕,JavaScript引擎开始执行微任务队列中的所有任务,输出"promise2"。
- 任务队列为空后,执行下一个宏任务(setTimeout的回调函数),输出"setTimeout"。
这个例子跟上个例子对比只是多了一步Promise的同步执行函数,很多人会把promise的同步执行函数跟.then() 微任务混淆,要记住Promise本身是**同步的立即执行函数**, 当在executor中执行resolve或者reject的时候, 此时是异步操作
。
例子3
来个难的,猜猜以下会输出什么
javascript
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
console.log(p)
}, 0)
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);
头昏眼花了吧,老弟。公布答案:3->7->4->1->2->5->8
, 哈哈哈,是否和你想的一致呢?如果是,那么恭喜你,已经掌握了事件循环的机制
了。如果不是,也不要气馁,多看几遍会开窍的,废话不多说,直接上解析:
要理解这段代码的执行顺序,我们需要首先明白几个关键点:
- Promise的构造函数是立即执行的,它里面的代码会同步运行。
setTimeout
是一个异步操作,它的回调函数会在当前执行栈清空后被放入事件队列,等待下一次事件循环执行。- Promise的
.then()
方法中的回调函数是异步执行的,当Promise的状态变为fulfilled(即resolved)时,这些回调函数会被放入微任务队列,等待当前执行栈清空后执行。
基于以上几点,我们可以分析代码的执行顺序:
- 首先同步执行Promise构造函数的代码,输出"3"。
- 接着执行内部Promise的构造函数,输出"7"。
- 同步执行console.log(4),输出"4"。
- 设置一个setTimeout,回调会被放入事件队列。
- 内部Promise的resolve立即执行,状态变为fulfilled。
- 外部Promise的resolve立即执行,状态变为fulfilled
- 添加内部Promise的.then()到微任务队列。
- 添加外部Promise的.then()到微任务队列。
- 执行微任务队列中的任务,输出"1"(内部Promise的resolve值)。
- 执行微任务队列中的任务,输出"2"(外部Promise的resolve值。
- 当微任务执行完成之后,执行事件队列中的宏任务,输出"5"。
- 这里的resolve(6)不会影响外部Promise的状态。
- 最后输出"8"。
通过这些示例,我们可以看到宏任务和微任务在JavaScript事件循环中的执行顺序。了解这一点,可以帮助我们更好地编写和管理异步代码,避免潜在的问题和错误。
在这里我斗胆出两道题目考考大家,知道的把答案打在评论区吧:
题目1
提示:注意定时器的延迟时间
javascript
const async1 = async () => {
console.log('async1');
setTimeout(() => {
console.log('timer1')
}, 2000)
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 end')
return 'async1 success'
}
console.log('script start');
async1().then(res => console.log(res));
console.log('script end');
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.catch(4)
.then(res => console.log(res))
setTimeout(() => {
console.log('timer2')
}, 1000)
题目2:
提示:注意宏任务,微任务进出队列顺序
javascript
Promise.resolve()
.then(() => {
console.log('Promise 1');
return new Promise(resolve => {
setTimeout(() => {
console.log('setTimeout 1');
resolve();
}, 0);
});
})
.then(() => {
console.log('Promise 2');
Promise.resolve().then(() => console.log('Promise 3'));
})
.then(() => {
console.log('Promise 4');
});
setTimeout(() => {
console.log('setTimeout 2');
Promise.resolve().then(() => console.log('Promise 5'));
}, 0);
console.log('Script End');
快来试试吧!