深入解析JS的宏任务和微任务

1、关于javascript

javascript是一门单线程语言,换而言之,在一段时间内,js只会做一件事,只有等这件事做完了,才会去做另外的事。但是任务分大小,分轻重缓急,如何能提高整体的效率是我们要思考的问题。

2、js的事件循环

比如我们做饭,完成做饭这个流程分为:烧水煮饭->洗菜切菜->炒菜等,难道我们要等烧水煮饭完成后再去做后面的事情吗?要知道“干饭不积极,思想有问题”,任何事情都是难不倒我们聪明的程序猿的。
我们把任务分为同步任务跟异步任务:

(1)同步:前一个任务结束后再执行后一个任务,程序的执行顺序与任务的排列顺序是一致的、同步的。比如做饭的同步做法:我们要烧水煮饭,等水开了(10分钟之后),再去切菜,炒菜。
(2)异步:在做一件事情时,因为这件事会花费很长时间,在做这件事的同时,你还可以去处理其他事情。比如做饭的异步做法:在烧水的同时,利用这10分钟,去切菜,炒菜。
本质区别:这条流水线上各个流程的执行顺序不同
同步任务:同步任务都在主线程上执行,形成一个执行栈
异步任务:JS的异步是通过回调函数实现的,一般而言,异步任务有以下三种类型:
(1)普通事件,如 click、resize等
(2)资源加载,如 load、error等
(3)定时器,包括 setInterval、setTimeout等

为了用最小的学习成本彻底弄懂执行机制,所以我们用导图来说明:
在这里插入图片描述
看完这张图后,小朋友,你是否有很多的疑惑?
在这里插入图片描述

同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
当指定的事情完成时,Event Table会将这个函数移入Event Queue。
主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
上述过程会不断重复,也就是常说的Event Loop(事件循环)。

话不多说,我们来直接上代码:

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
promise1
script end
promise2
setTimeout

看到这段代码,我相信很多朋友会拿起键盘,哒哒哒敲下。
让我们来更加详细地了解下,JS执行机制:
(1)先执行执行栈中的同步任务
(2)当有异步任务(回调函数)时,提交给对应的异步进程处理,比如onclick,当你点击了,才会放入任务队列中
(3)一旦执行栈中的同步任务执行完毕,系统就会按次序读取任务队列中的异步任务,于是被读取的异步任务结束等待状态,进入执行栈,开始执行。

>         console.log(1);
>         setTimeout(function(){
>             console.log(3);
>         },0);
>         console.log(2);
>         // 结果为 1 2 3
console.log(1);
document.onclick = function(){
     console.log('click');
 }
 console.log(2);
 // 3秒之后才会提交到任务队列中
 setTimeout(function(){
     console.log(3);
 },3000);
 
 不点击输出的结果是 123
 3秒之前点击的结果是 12,click,3
 3秒之后点击的结果是 123,click

看到这里,js的执行机制你懂了吗?
像极了此时自信满满的你
在这里插入图片描述我们进入正题,除了广义的同步任务和异步任务,我们对任务有更精细的定义:

1、macro-task(宏任务):进入任务栈等待主线程执行的主代码块,包括从异步队列里加入到栈的,如setTimeout,setInterval
2、micro-task(微任务):是异步队列中,在当前这一次宏任务执行完后,页面渲染之前要执行的任务。如Promise,process.nextTick,Object.observe

没有对比就没有伤害
在这里插入图片描述

让我们来看看宏任务和微任务的执行过程:

1.主体代码(第一次事件循环开始,所有的script代码)作为宏任务进入任务执行栈,但在主线程执行之前要做一系列操作判断。
2.判断当前任务是同步还是异步,同步的由主线程在任务栈中按先进后出顺序(先局部上下文,再全局上下文)执行,异步判断是宏任务还是微任务。
3.异步中的宏任务放入异步的宏任务event Table(异步队列分两种,宏任务队列和微任务队列,event Table也一样),微任务进入微任务event Table,在回调函数注册之后,再次进入它们对应的队列。
4.当主线程的任务执行完后,会检查微任务队列是否有任务,如果有就执行,如此循环,直到微任务队列没有任务。
5.当前事件的微任务执行完后,开始执行下一次事件,即会执行宏任务队列中的宏任务,如此循环下去,直到没有任务。

下面用代码来深入理解上面的机制:

console.log('---start---');//第一轮主线程
setTimeout(() => {
  console.log('setTimeout');  // 将回调代码放入个宏任务队列,第二轮宏任务执行
}, 0);
 
new Promise((resolve, reject) => {
  console.log('---Promise第一轮微任务同步执行---');//第一轮微任务同步执行
  resolve()
}).then(()=>{
  console.log('Promise.then实例成功回调执行'); // 将回调代码放入微任务队列,第一轮宏任务执行完后立即执行
});
 
console.log('---end---');//第一轮主线程结束

在这里插入图片描述

1、这段代码作为宏任务,进入主线程。
2、先遇到同步代码console.log(),立即执行
3、接下来遇到setTimeout ,将回调代码放入个宏任务队列
4、遇到Promise,它在创建的时候就会立即执行,then函数分发到微任务队列
5、遇到最后一个console.log(), 立即执行
5、主线程上所有代码执行完毕, 查看当前有没有可执行的微任务,执行then的回调。
6、微任务队列空了,执行下一个宏任务

其实不管代码怎么嵌套,怎么复杂,只要我们弄清除了原理,什么都不怕。让我们再来个复杂一点的练练手吧

 1 new Promise(function (resolve) { 
 2     console.log('1')// 宏任务一
 3     resolve()
 4 }).then(function () {
 5     console.log('3') // 宏任务一的微任务
 6 })
 7 setTimeout(function () { // 宏任务二
 8     console.log('4')
 9     setTimeout(function () { // 宏任务五
10         console.log('7')
11         new Promise(function (resolve) {
12             console.log('8')
13             resolve()
14         }).then(function () {
15             console.log('10')
16             setTimeout(function () {  // 宏任务七
17                 console.log('12')
18             })
19         })
20         console.log('9')
21     })
22 })
23 setTimeout(function () { // 宏任务三
24     console.log('5')
25 })
26 setTimeout(function () {  // 宏任务四
27     console.log('6')
28     setTimeout(function () { // 宏任务六
29         console.log('11')
30     })
31 })
32 console.log('2') // 宏任务一

结果:1-2-3-4-5-6-7-8-9-10-11-12

初步总结:先执行主线程上的同步代码,再执行微任务队列,后执行宏任务队列。

注意事项:

(1)promise是立即执行的,它创建的时候就会执行,不存在将promise推入微任务中的说法;
(2)resolve()是用来表示promise的状态为fullfilled,相当于只是定义了一个有状态的Promise,但是并没有调用它;
(3)promise调用then的前提是promise的状态为fullfilled;
(4)只有promise调用then的时候,then里面的函数才会被推入微任务中;
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210718002617136.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzQ2NjEzNDI5,size_16,color_FFFFFF,t_70#pic_center

3、async和await

抛出3个疑问: 1、async是干啥的? 2、await在等啥? 3、await等到了又要干啥?

async/await是对Promise的优化,await后面需要是一个Promise对象,如果不是则会被转成Promise对象
在这里插入图片描述

执行顺序:
(1)await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成;
(2)若 Promise 正常处理(Resolved,又称Fulfilled),其回调resolve函数的参数作为 await 表达式的值,继续执行 async function;
(3)若 Promise 处理异常(Rejected),await 表达式会把 Promise 的异常原因抛出;
可能还是会有点懵,话不多说,我们直接上代码

console.log('script start');
async function async1() { //语法糖 async2()执行完毕 才执行下面 会加入在微任务里面
      await async2();
      console.log('async1 end');
  };
  async function async2() {
      console.log('async2 end')
  }
  async1()
  setTimeout(function () {
      console.log("setTimeout100")
  }, 100)
  setTimeout(function () {
      console.log("setTimeout")
  }, 0)
  new Promise(resolve => {
      console.log('promise')
      resolve()
  }).then(function () {
      console.log("promise1")
  })
      .then(function () {
          console.log("promise2")
      })

在这里插入图片描述
上面代码中async1遇到await async2()会等async2()执行完毕再把下面的加入到微任务队列里面,所以先输出的是async2 end,由于下面的promise是立即执行的,所以先输出promise再输出async1 end
Promise和async中的立即执行

我们知道Promise中的异步体现在then和catch中,所以写在Promise中的代码是被当做同步任务立即执行的。而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?
await做了什么,从字面意思上看await就是等待,await等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。如果里面没有await,执行起来等同于普通函数;如果没有await,async函数并没有很厉害是不是

有人可能会想async没有await的情况下与其它的同步代码谁先执行的问题,让我们直接上代码

console.log('script start');
async function async2() {
    console.log('async2 end')
}
async2()
console.log('abc');

在这里插入图片描述

console.log('script start');
async function async1() {
    await async2();
    console.log('async1 end');
};
async function async2() {
    console.log('async2 end')
}
console.log('abc');
async1()

在这里插入图片描述
有的小伙伴看到上面可能有点懵了,不是说async也是立即执行的吗,async2 不是先出现了,应该先输出async2 end再输出abc吗?要看清楚哦,上面只有先调用了async1()才会调用async2,而console.log(‘abc’)在async2调用之前执行呢?所以还是要看清楚的,来给你验证下:

console.log('script start');
async function async1() {
    await async2();
    console.log('async1 end');
};
async function async2() {
    console.log('async2 end')
}
async1();
console.log('abc');

在这里插入图片描述

解决开头的3个疑问:

1、async是干啥的?
定义异步函数(内部通常有异步操作),返回Promise对象(函数返回Promise→显式返回return的Promise;函数返回非Promise→隐式返回Promise.resolve()包装的return值;)
2、await在等啥? 只能放在async函数中,等待右侧表达式结果(函数→结果=return值;字符串→结果=字符串;)
3、await等到了又要干啥?
阻塞后面的代码,先执行async外部的同步代码,同步代码执行完再回到async内部,拿到运算结果(表达式返回Promise→等待Promise对象fulfilled,再将resolve参数作为表达式的运算结果;表达式返回非Promise→直接作为表达式的运算结果;)

  • 12
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值