Event Loop 事件循环机制 & Promise async/await 执行顺序问题

Event Loop

Event Loop 也叫事件循环机制。
为解决 js 单线程而产生的异步问题,又出现了各种方式来实现异步:

  1. callback 回调函数;
  2. Promise:为解决回调地狱问题应运而生;
  3. Generator:通过yield关键字暂停和恢复函数的执行;
  4. async/await:Promise 和 Generator 的语法糖,解决 Promise.then 的链式调用,使代码看起来更像是同步代码

Event Loop 分为浏览器端和 nodejs 端,这里主要介绍的浏览器端的同步代码和异步代码的执行顺序问题。

宏任务和微任务

js 执行任务分为宏任务和微任务,宏任务放在任务队列,微任务放在微任务队列,先进先出(FIFO)。
微任务:Promise(then、catch、finally)、MutationObserver、Process.nextTick、defineProperty、Proxy;
宏任务:script、setTimeout、I/O、所有的网络请求

其中:defineProperty 和 Proxy 可被监听的数据拦截,本质上都是通过回调处理的,不是立刻执行的。

Promise

Promise 是同步代码,then、catch、finally 才是异步

	new Promise(function (resolve) {
	  console.log('promise1') // 同步
	  resolve();
	}).then(function () {
	  console.log('promise2') // 异步
	})

async/await

await 之前的代码可以看做是同步的,await 右侧紧跟着的,也可以看作是同步的。await 下面的,被阻塞,整体看作是 Promise.then() 微任务

	async function async1() {
      console.log('async1 start') // 同步
      await async2() // 同步
      console.log('async1 end') // 异步
    }
    async function async2() {
      console.log('async2')
    }

Event Loop执行顺序

口诀:有微则微,无微则宏

  1. 先执行整个 script (宏任务),从而往下依次执行;
  2. 如果遇到同步任务直接进入主线程立即执行;如果遇到异步任务,宏任务放在任务队列,微任务放在微任务队列,待主线程空闲后,从任务队列中从前往后依次取出任务 tick 的回调函数进入主线程执行;
  3. 主线程清空后,先看当前层级有没有微任务,有的话就拿到主线程执行,直到清空微任务队列,再去执行下一层宏任务;
  4. 再下一层宏任务中,依旧按照上面的执行顺序,重复第2步和第3步。即先同步,清空后,再微任务,遇到宏任务,再加到下一轮,以此类推……

实战演练

有了上面了知识,来实操一下。

先看一个经典面试题:

	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) { // S14
	  console.log('promise1')
	  resolve();
	}).then(function () {
	  console.log('promise2')
	})
	console.log('script end')

这段代码的执行结果是什么?我们来分析一下:

  • 首先遇到 async function async1async function async2 的函数定义,加了 async 好像很厉害的样子,再厉害也遵循函数不调用就不执行的原则;

  • 接下来 console.log('script start'); 直接控制台打印script start
    控制台如下:
    在这里插入图片描述

  • 接着将 console.log( setTimeout ) 的回调函数,在0秒后将放进任务队列,标记为 H1

  • 接下来 async1() 调用函数后,开始执行 async1函数,控制台打印 async1 start,遇到 await async2(),await 后面的也是同步代码,控制台打印 async2,await 下面的 console.log('async1 end'),看做 Promise.then( console.log('async1 end') ),放进微任务队列,标记为 W1,此时async1() 处理结束;
    控制台如下:
    在这里插入图片描述

  • 后面来到 S14 行(代码中有注释) new Promise(function (resolve) { ... } ,控制台打印 promise1,遇到 resolve(),将 then 里面回调放进微任务队列,标记为 W2;
    控制台如下:
    在这里插入图片描述

  • 接着来到最后一行 console.log('script end'),控制台打印 script end
    控制台如下:
    在这里插入图片描述

以上:第一轮宏任务执行完毕

  • 接着要去看这轮宏任务中有无产生的微任务,很显然有 W1,W2,按先入先出原则,依次执行,控制台依次打印 async1 endpromise2,微任务队列清空;
    控制台如下:
    在这里插入图片描述

  • 去看宏任务,作为第二轮宏任务执行,执行 H1,控制台打印 setTimeout

  • 至此,所有代码执行完毕,完整控制台如下:
    执行结果图

小Tips

在 Chrome72 版本之前的版本,上面例子输出如下:

VM56:15 script start
VM56:2 async1 start
VM56:7 async2
VM56:9 promise3
VM56:21 promise1
VM56:26 script end
VM56:12 promise4
VM56:24 promise2
VM56:4 async1 end
undefined
VM56:17 setTimeout

async1 endpromise2 之后输出。
原因:在 Chrome72 版本之后浏览器 ECAM 规范有修改。
减少了 await 任务的循环次数,从而提高效能,通过直接调用 PromiseResolve() 代替之前的又一层 Promise 封装。
代码片段

可参考 https://github.com/tc39/ecma262/pull/1250

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值