理清浏览器下的事件循环机制(Event Loop)

我们知道,JavaScript作为浏览器的脚本语言,起初是为了与用户交互和操作DOM,为了避免因为同时操作了同一DOM节点而引起冲突,被设计成为一种单线程语言。

而单线程语言最大的特性就是同一时间只能做一件事,这个任务未完成下一个任务就要等待,这样无疑是对资源的极大浪费,而且严重时会引起阻塞,造成用户体验极差。这个时候就引出了异步的概念,而异步的核心就是事件循环机制Event Loop。

何为事件循环机制?

JavaScript的任务分两种,分别是同步任务和异步任务。

  • 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  • 异步任务:不进入主线程而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程某个异步任务可以执行了,该任务才会进入主线程执行。

clipboard.png

如上图所示:

  1. 主线程在执行代码的时候,遇到异步任务进入Event Table并注册回调函数,有了运行结果后将它添加到事件队列(callback queue)中,然后继续执行下面的代码,直到同步代码执行完。
  2. 主线程执行完同步代码后,读取callback queue中的任务,如果有可执行任务则进入主线程执行

不断重复以上步骤,就形成了事件循环(Event Loop)

clipboard.png

<script>
console.log('start')
setTimeout(function () {
  console.log('setTimeout')
}, 0)
  
console.log('end')
</script>

结合上面步骤分析下这个例子:

1. 执行主线程同步任务,输出start【1】,继续往下执行
2. 遇到setTimeout,进入event table注册setTimeout回调,setTimeout回调执行完后,继续往下执行
3. 输出end【2】,同步任务执行完毕
4. 进入event queue,检查是否有可执行任务,取出event queue中setTimeout任务开始执行,输出setTimeout【3】

结果依次为:start -> end -> setTimeout

浏览器环境下的异步任务

在浏览器和node中的事件循环与执行机制是不同的,要注意区分,不要搞混。

执行过程

浏览器环境的异步任务分为宏任务(macroTask)和微任务(microtask),当满足条件时会分别被放进宏任务队列和微任务队列(先进先出),等待被执行。

  • 微任务:
    promise,MutationObserver
  • 宏任务:
    script整体,setTimeout & setIntervat,I/O,UI render。

执行过程如下:

clipboard.png

如图所示:

1. 把整体的script代码作为宏任务执行
2. 执行过程中如果遇到宏任务和微任务,满足条件时分别添加至宏任务队列和微任务队列
3. 执行完一个宏任务后,取出所有微任务依次执行,如果微任务一直有新的被添加进来,则一直执行,直到把微任务队列清空
4. 不断重复2和3,直到所有任务被清空,结束执行。

clipboard.png

<script>
console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(() => {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(() => {
    console.log('promise2')
  })
}, 0)
setTimeout(() => {
  console.log('timer3')
  Promise.resolve().then(() => {
    console.log('promise3')
  })
}, 0)
new Promise(function(resolve) {
    console.log('promise4');
    resolve();
}).then(function() {
    console.log('promise5')
})

console.log('end')
</script>

分析:

  • 第一轮:

    1. 输出start【1】,将setTimeout回调函数@1,放进宏任务队列;
    2. 将setTimeout回调函数@2,放进宏任务队列;
    3. 将setTimeout回调函数@3,放进宏任务队列;
    4. 执行new Promise函数输出promise4【2】,将Promise.then@1放进微任务队列;
    5. 输出end【3】,此时队列如下所示:
      clipboard.png
    6. 第一轮宏任务执行完毕,开始执行微任务,取出微任务Promise.then@1,输出promise5【4】,此时微任务队列被清空,开始第二轮执行。
  • 第二轮:

    1. 取出宏任务setTimeout回调函数@1,输出timer1【5】,将回调函数中的Promise.then@2放进微任务队列;
    2. 宏任务setTimeout回调函数@1中无宏任务,开始执行微任务,取出Promise.then@2,输出promise1【6】,此时:

      clipboard.png

    3. setTimeout回调函数@1中宏任务队列和微任务队列均被清空,开始第三轮执行
  • 第三轮:

    1. 取出宏任务setTimeout回调函数@2,输出timer2【7】,将Promise.then@3放进微任务队列;
    2. setTimeout回调函数@2中无宏任务,开始执行微任务,取出Promise.then@3,输出promise2【8】,此时:

      clipboard.png

    3. 宏任务setTimeout回调函数@2中宏任务队列和微任务队列均被清空,开始第四轮执行
  • 第四轮:

    1. 取出宏任务setTimeout回调函数@3,输出timer3【9】,将Promise.then@4放进微任务队列;
    2. setTimeout回调函数@3中无宏任务,开始执行微任务,取出Promise.then@4,输出promise3【10】

现在宏任务对列和微任务队列都被清空了,完成执行,结果为:start > promise4 > end > promise5 > timer1 > promise1 > timer2 > promise2 > timer3 > promise3

引入 async/await

asnyc知识点传送门

await表达式的运算结果取决于它右侧的结果

当遇到await时,会阻塞函数体内部处于await后面的代码,跳出去执行该函数外部的同步代码,当外部同步代码执行完毕,再回到该函数内部执行剩余的代码

补充aynsc的一点知识:

如果aynsc函数中return一个直接量,async 会把这个直接量通过Promise.resolve()封装成Promise对象,如果什么都没return,会被封装成Promise.resolve(undefined)

那么 引入了async await之后的执行过程是怎样的呢?

clipboard.png

<script>
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}

async function async2() {
  console.log("async2");
}

console.log("script start");

setTimeout(function () {
  console.log("setTimeout");
}, 0);

async1();

new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("promise2");
});

console.log("script end");
</script>

分析:

  • 第一轮:

    1. 执行同步代码,输出:script start【1】,将setTimeout回调@1放入宏任务队列;
    2. 进入aynsc1函数中,执行同步代码输出:async1 start【2】,遇到await从右向左执行,进入async2函数,输出:async2【3】;aynsc2函数体中未返回任何东西等价于返回了Promise.resolve(undefined),拿到返回值后进入aynsc1函数体中,继续执行剩下的部分,这时候aynsc1中注释部分等价于:

      async function async1() {
        console.log("async1 start");
        //await async2();
        //console.log("async1 end");
         await new Promise((resolve) => resolve()).then(resolve => {
           console.log('async1 end')
         })
      }

      将Promise.then@1推入到微任务队列;

    3. 继续执行同步代码,输出:promise1【4】,将Promise.then@2推入微任务队列
    4. 继续执行同步代码,输出:script end【5】,第一轮宏队列任务执行完毕,此时如下:

      clipboard.png

    5. 开始执行微任务,取出微任务Promise.then@1,值为undefined,这个时候Promise.then@1完成执行,则await aynsc2()得到了值也完成了执行,不再阻塞后面代码,那么执行同步代码输出:async1 end【6】;
    6. 取出微任务Promise.then@2,输出:promise2【7】,微任务全部执行完毕,现在开始第二轮执行
  • 第二轮:

    1. 取出宏任务队列中的setTimeout@1,输出setTimeout【8】

所有任务队列均为空,结束执行,输出结果为:script start > async1 start > async2 > promise1 > script end > async1 end > promise2 > setTimeout

补充谷歌浏览器测试结果:

clipboard.png

clipboard.png 借用一个例子:await一个直接值的情况

<script>
console.log('1')
async function async1() {
  console.log('2')
  await 'await的结果'
  console.log('5')
}

async1()
console.log('3')

new Promise(function (resolve) {
  console.log('4')
  resolve()
}).then(function () {
  console.log('6')
})
</script>

分析:

  • 第一轮:

    1. 执行同步函数,输出:1【1】,进入async1函数中,输出:2【2】,这个时候await虽然接收了一个直接值,但是还是要先执行外边的同步代码之后才能执行await后边的值
    2. 继续执行同步代码,输出:3【3】,进入Promise函数,输出:4【4】,将Promise.then推入微任务队列
    3. 同步代码执行完毕,进入 async1函数中输出:5【5】
    4. 宏任务执行完毕,进入微任务队列,开始执行微任务;取出Promise.then,输出:6【6】

任务队列为空,执行完毕,结果为: 1 > 2 > 3 > 4 > 5 > 6

clipboard.png再借个例子,这个有点复杂

<script>
setTimeout(function () {
  console.log('8')
}, 0)

async function async1() {
  console.log('1')
  const data = await async2()
  console.log('6')
  return data
}

async function async2() {
  return new Promise(resolve => {
    console.log('2')
    resolve('async2的结果')
  }).then(data => {
    console.log('4')
    return data
  })
}

async1().then(data => {
  console.log('7')
  console.log(data)
})

new Promise(function (resolve) {
  console.log('3')
  resolve()
}).then(function () {
  console.log('5')
})
</script>

分析:

  • 第一轮:

    1. 将setTimeOut@1放入宏任务列队;
    2. 执行async1()函数体内的函数,输出:1【1】,遇到await,进入aynsc2函数体,输出:2【2】,将该函数体内promise.then@1放入微任务队列中;
    3. 执行New promise .. 输出3【3】,将该函数体内Promise.then@2放入微任务队列中,第一轮宏任务执行完毕,此时:

      clipboard.png

    4. 开始执行第一轮微任务,取出Promise.then@1,输出:4【4】,此时async2函数执行完毕,进入aynsc1函数,此时改动下aynsc1函数,等价于:

      async function async1() {
        console.log('1')
        //const data = await async2()
        //console.log('6')
         const data = await new Promise(resolve => resolve('async2的结果')).then((resolve) => {
                          console.log(6); 
                          return resolve;
                      })
      
         return data;
      }

      将上面promise.then@3推入微任务队列中,此时:

      clipboard.png

    5. 接着执行微任务,取出promise.then@2,输出:5【5】,取出promise.then@3,输出:6【6】,此时函数async1执行完成,接着执行async1().then(...),将async1().then@1推到微任务队列中,取出async1().then@1,输出:7【7】和 'async2的结果'【8】;
    6. 第一轮任务执行完毕,开始执行第二轮,此时:

      clipboard.png

  • 第二轮:

    1. 开始执行第二轮宏任务,将setTimeOut@1取出执行,输出8【9】,完毕。

所以任务被执行完毕,结果为:1 > 2 > 3 > 4 > 5 > 6 > 7 > async2的结果 > 8

------------------------ END ----------------------------

PS: 好记性不如烂笔头,看了那么多资料,还是想总结一下,不然过一阵子就忘记了,如果辛苦指出哦,谢谢~

参考资料:

理解 JavaScript 的 async/await
浏览器和Node不同的事件循环(Event Loop)
Event Loop 原来是这么回事
这一次,彻底弄懂 JavaScript 执行机制
从event loop到async await来了解事件循环机制
...

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值