宏任务与微任务傻傻分不清?看这篇文章就够了

宏任务与微任务傻傻分不清?看这篇文章就够了

在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');

快来试试吧!

相关探索