宏任务与微任务执行顺序(超详细讲解)

前言

写此之前,也查阅了很多文章,并结合自己的理解,说说对Event Loop模型的理解、以及对Promise、async/await在任务队列中的影响进行了分析,也给出了多种情形的任务案例以及分析解释,相信大家看完会有所收获;当然,也是自己的理解,难免有所偏差,欢迎大家指正~

一、 宏任务、微任务的基本概念

  1. 宏任务、微任务发展及出现的原因:

JavaScript是典型的单线程(自上而下依次执行代码),但是,一个任务耗时过长,或多个任务需要执行时,势必导致线程阻塞,影响视图渲染效果。

在ES3以及以前的版本中,JavaScript本身没有异步任务能力,随着Promise的引入,JavaScript引擎自身也能够发起异步任务了。至此,JavaScript就可分为同步任务及异步任务;而JS又把异步任务做了进一步的划分,分为宏任务与微任务。

由于微任务执行快,一次性可以执行很多个,在当前宏任务执行后立刻清空微任务可以达到伪同步的效果,这对视图渲染效果起到至关重要的作用,这也是区分宏任务、微任务的原因。

  1. 宏任务、微任务的基本类型

宏任务:

  • script(外层同步代码)

  • Ajax请求

  • setTimeout、setInterval

  • postMessage、MessageChannel

  • setImmediate(Node.js 环境)、I/O(Node.js 环境)

  • ...

微任务

  • Promise.then().catch() 和 .finally()

  • process.nextTick(Node.js 环境)

  • ...

二、 事件循环模型(Event Loop)

如上图,当同步代码执行完毕后,就会执行所有的宏任务,宏任务执行完成后,会判断是否有可执行的微任务;如果有,则执行微任务,完成后,执行宏任务;如果没有,则执行新的宏任务,形成事件循环。

这只是图示宏任务及微任务的执行关系,那么,js在 Event Loop中究竟是如何调用方法去处理的呢?

  1. 我们写的代码,js会进行任务类型分配,根据类型放入不同的任务队列中;

  1. Event Loop 会根据执行时机,将需要执行的函数,压入事件调用堆栈中执行;

  1. 相同的宏任务,也会有不同的执行时机,(类似 setTimeout 的 time),Event Loop 会控制将需要执行的函数压入。

总结:Event Loop 在压入事件时,都会判断微任务队列是否还有需要执行的事件:如果有,则优先将需要执行的微任务压入;没有,则依次压入需要执行宏任务!

切记,宏任务执行完毕后,都会判断是否还有需要执行的微任务!!!在复杂的事件中,该点经常会错!!!

切记,宏任务执行完毕后,都会判断是否还有需要执行的微任务!!!在复杂的事件中,该点经常会错!!!

切记,宏任务执行完毕后,都会判断是否还有需要执行的微任务!!!在复杂的事件中,该点经常会错!!!

三、 Promise、async和await 在事件循环中的处理

  1. Promise:

new Promise 创建实例的过程是同步的哦!

 console.log(1);
 new Promise((resolve, reject) => {
    console.log(2);
 })
 console.log(3);
// 结果: 1 2 3

但是,Promise.then().catch().finally()中的回调,是微任务。

 console.log(1);
 new Promise((resolve, reject) => {
    console.log(2);
    resolve(); // 触发 then 回调
    // reject(); // 触发 catch 回调
 }).then(()=>{
    console.log('then')
 }).catch(()=>{
    console.log('catch')
}).finally(()=>{
    console.log('finally')
})
 console.log(3);
// 结果: 1 2 3 then finally

上图是菜鸟教程-JavaScript Promise 中对Promise的讲解,我们想一下为啥要这样设计:

我们假设Promise 不是立即执行的,会有什么后果?利用Promise,多是封装异步请求(Ajax),而请求不是立即请求的,还需要等待Promise 任务执行,那么我们就失去了网络请求的时效性,会导致页面等待渲染(因为我们上面提及的 Event Loop 会根据事件执行时机,选择将事件压入堆栈中,我们无法保证自己写的【不是立即执行Promise】什么时候执行)。

  1. async/await :

简单来说,async是通过Promise包装异步任务。

async function fun1() {
  console.log('fun1 start')
  await fun2(); // 等待 fun2 函数执行完成
  console.log('fun1 end')
}
async function fun2() {
  console.log('fun2 start')
  console.log('fun2 end')
}
fun1()

// 输出结果: fun1 start、fun2 start、fun2 end、fun1 end

遇到 await 则需要等待 await 后的代码执行完成后,在往下执行代码。因此,可以将 await 看作抢夺线程的标记,fun1 中,本来是同步执行的,但是 await 的出现,导致线程执行了 fun2 的代码后,再次回到await,往后执行。

同时,await后面的代码,会进入then微任务中!!!

同时,await后面的代码,会进入then微任务中!!!

同时,await后面的代码,会进入then微任务中!!!

async function fun1() {
  console.log('fun1 start')
  await fun2(); // 等待 fun2 函数执行完成
  console.log('我是 await 后面的代码')
  console.log('fun1 end')
}
async function fun2() {
  console.log('fun2 start')
  console.log('fun2 end')
}
fun1()
console.log('await 阻塞,导致 await后面代码进入 then 微任务')

这个知识点比较容易忽略,以为 await 回来后,直接继续执行后面的代码,这是不对的!

四、 process.nextTick在事件循环中的处理

process.nextTick是Node环境的变量,process.nextTick() 是一个特殊的异步API,其不属于任何的Event Loop阶段。事实上Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick(),这个执行完后才会继续Event Loop。所以,nextTick和Promise同时出现时,肯定是process.nextTick() 先执行

可以类比 Vue.$nextTick(),也是需要执行完这个函数后,才能继续Event Loop。

五、 典型案例

5.1 典型的宏任务、微任务

setTimeout(function () {
    console.log('1');
});

new Promise(function (resolve) {
    console.log('2');
    resolve();
})
  .then(function () {
      console.log('3');
  })
  .then(function () {
      console.log('4')
  });
        
console.log('5');

5.2 宏任务中包含微任务

setTimeout(function () {
    console.log('1');
    new Promise(function (resolve) {
        console.log('2');
        resolve();
    })
      .then(function () {
          console.log('3');
      })
    console.log('4');
});

console.log('5');

5.3 微任务中包含宏任务

console.log('1');
new Promise(function (resolve) {
    console.log('2');
    resolve();
})
  .then(function () {
      console.log('3');
      setTimeout(function () {
          console.log('4');
       })
      console.log('5');
  })

console.log('6');

5.4 async 宏任务

async function fun1() {
  console.log('fun1 start')
  setTimeout(function () {
      console.log('fun1 setTimeout');
  });
  await fun2();
  console.log('fun1 end')
}

async function fun2() {
  console.log('fun2 start')
  new Promise((resolve)=>{
      console.log('fun2 Promise')
      resolve()
  })
    .then(()=>{
        console.log('fun2 Promise then')
    })
  console.log('fun2 end')
}

fun1()

5.5 node 事件执行分析

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

5.6 综合案例

console.log('script start')

async function fun1() {
  console.log('fun1 start')
  process.nextTick(function() {
    console.log('fun1 process nextTick');
  })
  setTimeout(function () {
      console.log('fun1 setTimeout');
      new Promise(function (resolve) {
          console.log('fun1 Promise');
          resolve();
      })
        .then(function () {
            console.log('fun1 Promise then');
            setTimeout(function () {
                console.log('fun1 Promise then setTimeout');
             })
            console.log('fun1 Promise then end');
        })
  });
  await fun2();
  console.log('fun1 end')
}

async function fun2() {
  console.log('fun2 start')
  setTimeout(function () {
      console.log('fun2 setTimeout');
  });
  new Promise((resolve)=>{
      console.log('fun2 Promise')
      resolve()
  })
    .then(()=>{
        console.log('fun2 Promise then')
    })
  console.log('fun2 end')
}

fun1()

setTimeout(function() {
  console.log('setTimeout-000')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  process.nextTick(function() {
    console.log('Promise process nextTick');
  })
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
    process.nextTick(function() {
      console.log('promise2 process nextTick');
    })
  })

console.log('script end')

六、案例分析

6.1 典型的宏任务、微任务

 // 同步任务
 console.log('2'); // new promise 实例化过程
 console.log('5');
 // 
 // 将 setTimeout console.log('1'); 放入宏任务队列
 // 将Promise 的回调放入微任务队列
 // then  console.log('3');
 // then  console.log('4')

 // 先微任务
 console.log('3');
 console.log('4')
// 再宏任务
 console.log('1');

// 因此,输出结果: 2 5 3 4 1

6.2 宏任务中包含微任务

// 同步任务
console.log('5');
// 将 setTimeout 放入宏任务队列,此时,没有微任务执行,因此,开始执行setTImeout宏任务
console.log('1');
// new Promise 实例化 同步执行:
console.log('2');
// 将Promise.then 回调放入微任务
// 当前(setTimeout)的宏任务事件还没有执行完!!!
// 注意哈!!当前(setTimeout)的宏任务事件还没有执行完!!!,事件未跳出当前 Loop
console.log('4');
// 执行完宏任务,开始执行 微任务
console.log('3');

// 因此,结果为: 5 1 2 4 3

6.3 微任务中包含宏任务

 // 同步代码:
console.log('1');
// new Promise 
console.log('2');
console.log('6');
// 微任务:Promise.then
console.log('3');
console.log('5');
// 结束当前 Loop[有上个例子,这个就不难理解了]
// 开启宏任务
console.log('4');

// 因此,结果为:1 2 6 3 5 4

6.4 async 宏任务

 // fun1
console.log('fun1 start')
// 将setTimeout 放入宏任务队列
//  await fun2(); 进入 fun2
console.log('fun2 start')
//   new Promise
console.log('fun2 Promise')
// 将 then 放入微任务
console.log('fun2 end') // 当前任务队列

// 有微任务,先执行微任务
console.log('fun2 Promise then')
// 回到 await 处
console.log('fun1 end') // 当前 fun1 队列
console.log('fun1 setTimeout'); // 最后的宏任务

6.5 node 事件执行分析

// 从上往下:
console.log('1'); // 同步代码
//  setTimeout 宏任务1
//  process.nextTick 微任务1
console.log('7'); // new Promise
// Promise.then 微任务2
// setTimeout 宏任务2
// -- 开始执行微任务
console.log('6');
console.log('8')

// -- 开始宏任务1 
console.log('2');
//  process.nextTick 微任务!!!
console.log('4'); // new Promise
// Promise.then 微任务!!!
// 到此,当前宏任务已执行完毕,有微任务,需要先执行微任务
console.log('3');
console.log('5')
        
// 执行宏任务 2
console.log('9');
//  process.nextTick 微任务
console.log('11');// new Promise
// Promise.then 微任务!!!
console.log('10');
console.log('12')

// 因此,结果为:1 7 6 8 2 4 3 5 9 11 10 12

6.6 综合案例

// 这个案例,就应用了 await 导致的 then 微任务细节,我第一次分析也错了
// 还涉及了process.nextTick node 的执行时机优先
         
// 开始分析:
        console.log('script start')
        // fun1() 进入 fun1
        console.log('fun1 start')
        // 生成微任务 process.nextTick(fun1)
        // 生成 宏任务 setTimeout (fun1)
        await fun2(); // 进入 fun2
        console.log('fun2 start')
        // 生成宏任务 setTimeout (fun2)
        console.log('fun2 Promise') // new Promise
        // 生成 Promise.then 微任务
        console.log('fun2 end')
        // !!!此时,fun2 已经有返回值了,不需要等待 fun2 中的事件执行,回到 await 处,被标记了 await .then 的微任务
        // 因此,执行主任务
        console.log('Promise') // new Promise
        // 生成 Promise process.nextTick 微任务
        // 生成 Promise1.then 微任务
        // 生成 Promise2.then 微任务
        console.log('script end')
        /**
         * 分析微任务队列
         * 1. 第一个微任务:process.nextTick(fun1)
         * 2. fun2 Promise.then // 容易漏
         * 3. await .then 的微任务 !!!!!!!!!!!!!
         * 4. Promise process.nextTick 微任务
         * 5. Promise1.then 微任务
         * 6. Promise2.then 微任务
         *
        */
        // 根据 Node process 优先级,先执行 process
        console.log('fun1 process nextTick');
        console.log('Promise process nextTick');
        console.log('fun2 Promise then')
        // await.then微任务[await 后的所有代码,如果还有任务,具体再分析即可]
        console.log('fun1 end')
        console.log('promise1')
        console.log('promise2') // 执行到这,又生成新的 process.nextTick 微任务,又先执行
        console.log('promise2 process nextTick');
        // 没有微任务了,开始执行宏任务
        console.log('fun1 setTimeout');
        console.log('fun1 Promise'); // 生成新的 promise.then 微任务,当前宏任务已执行完成,开始执行微任务
        console.log('fun1 Promise then'); // 生成新的 宏任务 fun1 Promise then setTimeout
        console.log('fun1 Promise then end');
        /**
         * 此时,分析宏任务队列
         * 1.  第一个 是 fun2 setTimeout
         * 2.  setTimeout(function () { console.log('setTimeout-000') }, 0)
         * 3.  fun1 Promise then setTimeout
         * */
        // 因此, 依次执行宏任务
        console.log('fun2 setTimeout');
        console.log('setTimeout-000')
        console.log('fun1 Promise then setTimeout');

这个案例比较复杂,某些事件容易漏掉,因此,建议大家手动勾起来,每一个事件都对应到事件队列中,这个案例,考察两个点,一个是 await的处理及 node.process 的优先级。大家弄懂这个案例,出去面试,手撕这种题目应该不是问题了。

好啦,以上就是对宏任务、微任务、事件循环、案例的整体整理,有不对的地方,欢迎指正呀!

猜你喜欢

转载自blog.csdn.net/weixin_47746452/article/details/129244590
今日推荐