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

在JavaScript的异步编程模型中,宏任务(MacroTask)和微任务(MicroTask)是两个核心概念。它们共同构成了JavaScript的事件循环(Event Loop),使得JavaScript能够非阻塞地执行异步操作。了解宏任务和微任务的工作原理,对于编写高效、可维护的异步代码至关重要。

一、宏任务(MacroTask)

宏任务是由宿主环境(例如浏览器或Node.js)提供的任务,通常包括:

  1. 整体代码执行(script) :在浏览器加载页面时,首先会执行整体的JavaScript代码。这个执行过程本身就是一个宏任务。
  2. setTimeout 和 setInterval:这两个是JavaScript中常用的定时器函数,用于在指定的时间后执行某个函数,或者每隔一段时间执行某个函数。当定时器时间到达时,会触发一个宏任务,将回调函数放入宏任务队列中等待执行。
  3. setImmediate(Node.js特有) :在Node.js环境中,setImmediate()函数用于在I/O事件完成后,但在其他宏任务(如setTimeout)之前执行回调函数。因此,它也可以被视为一个宏任务。
  4. I/O 操作:输入/输出操作,如读取文件、网络请求等,通常也是宏任务。当I/O操作完成时,会触发相应的回调函数,并将这些回调函数作为宏任务放入任务队列中。
  5. UI渲染:在浏览器中,当页面需要重绘或重排时,也会触发宏任务。这些任务通常与页面的渲染性能相关。
  6. 事件回调:包括DOM事件(如点击、滚动等)和Web API事件(如XMLHttpRequest完成等)。当这些事件发生时,会触发相应的回调函数,并将这些回调函数作为宏任务放入任务队列中。

当JavaScript引擎开始执行代码时,会首先执行同步代码,然后将异步代码(如setTimeout、setInterval等)放入宏任务队列中等待执行。

二、微任务(MicroTask)

微任务是由JavaScript引擎自己维护的任务队列:

  1. Promise 的回调:在 JavaScript 中,Promise 用于处理异步操作。当 Promise 的状态从 pending 变为 resolved 或 rejected 时,会执行相应的回调函数,这些回调函数就可以被视为微任务。
  2. async/await:这是基于 Promise 的语法糖,用于简化异步代码的书写。当使用 async/await 编写的异步函数执行完毕后,其后续的代码(包括 then 或 catch 中的回调函数)也会作为微任务执行。
  3. MutationObserver:在浏览器中,MutationObserver 用于监听 DOM 树的变化。当监听到变化时,会触发回调函数,这些回调函数也是微任务。
  4. process.nextTick(Node.js 独有):在 Node.js 中,process.nextTick() 方法用于将回调函数放在当前执行栈的末尾,即在当前同步任务执行完毕后立即执行,它的优先级比 Promise 的回调要高。
  5. Object.observe(已废弃):虽然 Object.observe 方法已被废弃,并被 Proxy 对象替代,但在它存在的时候,用于监听对象属性的变化,并在变化时触发回调函数,这些回调函数也是微任务。

当JavaScript引擎执行完一个宏任务后,会检查微任务队列是否有待执行的微任务,如果有,则清空微任务队列中的所有任务,然后再执行下一个宏任务。

三、宏任务与微任务的执行顺序

JavaScript引擎在执行异步代码时,会按照以下顺序进行处理:

  1. 执行一个宏任务(通常是整体代码)。
  2. 执行完宏任务后,检查并执行所有微任务。
  3. 重复上述步骤,直到宏任务队列和微任务队列都为空。

四、示例

下面是一个简单的示例,演示了宏任务微任务的执行顺序:

例子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

是否跟你想的一致呢?接下来我们解析一下吧,其实聪明的你看到注释应该已经明白一切了。

  1. 首先执行同步代码,输出“script start”。
  2. 然后遇到setTimeout,将其回调函数放入宏任务队列。
  3. 接着遇到Promise的then方法,将其回调函数放入微任务队列。
  4. 继续执行同步代码,输出“script end”。
  5. 此时,一个宏任务执行完毕,JavaScript引擎开始执行微任务队列中的所有任务,输出“promise1”和“promise2”。
  6. 微任务队列为空后,执行下一个宏任务(setTimeout的回调函数),输出“setTimeout”。

例子2

这次没注释了喔,再来猜猜看会输出什么

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 这次的你是否跟答案想的一致呢,如果一致那么恭喜你,你已经掌握区分宏任务微任务的能力了,接下来我们剖析下代码的执行顺序。

  1. 首先执行同步代码,输出“script start”。
  2. 然后遇到Promise,执行里面的回调,输出“promise1”;“promise1 end”。
  3. 接着遇到Promise的then方法,将其回调函数放入微任务队列。
  4. 继续执行同步代码,然后遇到setTimeout,将其回调函数放入宏任务队列。
  5. 继续执行同步代码,输出“script end”。
  6. 此时,一个宏任务执行完毕,JavaScript引擎开始执行微任务队列中的所有任务,输出”promise2“。
  7. 任务队列为空后,执行下一个宏任务(setTimeout的回调函数),输出“setTimeout”。

这个例子跟上个例子对比只是多了一步Promise的同步执行函数,很多人会把promise的同步执行函数跟.then() 微任务混淆,要记住Promise本身是**同步的立即执行函数**, 当在executor中执行resolve或者reject的时候, 此时是异步操作

例子3

来个难的,猜猜以下会输出什么

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, 哈哈哈,是否和你想的一致呢?如果是,那么恭喜你,已经掌握了事件循环的机制了。如果不是,也不要气馁,多看几遍会开窍的,废话不多说,直接上解析:

要理解这段代码的执行顺序,我们需要首先明白几个关键点:

  1. Promise的构造函数是立即执行的,它里面的代码会同步运行。
  2. setTimeout是一个异步操作,它的回调函数会在当前执行栈清空后被放入事件队列,等待下一次事件循环执行。
  3. Promise的.then()方法中的回调函数是异步执行的,当Promise的状态变为fulfilled(即resolved)时,这些回调函数会被放入微任务队列,等待当前执行栈清空后执行。

基于以上几点,我们可以分析代码的执行顺序:

  1. 首先同步执行Promise构造函数的代码,输出”3“。
  2. 接着执行内部Promise的构造函数,输出”7“。
  3. 同步执行console.log(4),输出”4“。
  4. 设置一个setTimeout,回调会被放入事件队列。
  5. 内部Promise的resolve立即执行,状态变为fulfilled。
  6. 外部Promise的resolve立即执行,状态变为fulfilled
  7. 添加内部Promise的.then()到微任务队列。
  8. 添加外部Promise的.then()到微任务队列。
  9. 执行微任务队列中的任务,输出”1“(内部Promise的resolve值)。
  10. 执行微任务队列中的任务,输出”2“(外部Promise的resolve值。
  11. 当微任务执行完成之后,执行事件队列中的宏任务,输出”5“。
  12. 这里的resolve(6)不会影响外部Promise的状态。
  13. 最后输出”8“。

通过这些示例,我们可以看到宏任务和微任务在JavaScript事件循环中的执行顺序。了解这一点,可以帮助我们更好地编写和管理异步代码,避免潜在的问题和错误。

在这里我斗胆出两道题目考考大家,知道的把答案打在评论区吧:

题目1

提示:注意定时器的延迟时间

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:

提示:注意宏任务,微任务进出队列顺序

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

快来试试吧!

原文链接:https://juejin.cn/post/7346021356679921727 作者:大码猴

(0)
上一篇 2024年3月14日 下午4:36
下一篇 2024年3月14日 下午4:47

相关推荐

发表回复

登录后才能评论