一道题来看 async/await、Promise、setTimeout

一道题来浅析 JavaScript 的事件循环

//一道经典面试题
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 start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
复制代码

这道题主要考察的是JS的eventloop的执行顺序问题、微任务、宏任务 与异步函数特性等,下面一一道来。

一、EventLoop、微任务、宏任务

1.JS执行顺序

JS是单线程语言。JS任务需要排队顺序执行,如果一个任务耗时过长(如:ajax请求、I/O操作等),会严重阻塞承勋的执行,基于此 将任务设计成了两类:

  • 同步任务
  • 异步任务

同步任务:执行在主线程上,同步程序"由上向下"依次压入栈中,先进先出,也就是所谓的执行栈

异步任务:当进行异步操作有结果返回时(如:ajax请求、I/O操作等),会在主线程之外,事件触发线程管理的任务队列之中放置一个事件。

当主线程的执行栈执行完全部同步任务时,执行栈中已经没有可以执行的任务时,引擎会读取异步任务队列,任务队列类似一个缓冲区,可以执行的任务会被移到执行栈,然后主线程调用执行栈的任务。。

总而言之,检查执行栈是否为空,以及确定把哪个任务加入执行栈的这个过程就是事件循环,而JS实现异步的核心就是事件循环EventLoop。

规范理解,浏览器至少有一个事件循环,一个事件循环至少有一个任务队,也就是可以有一个或者多个任务队列(task queue),每个任务都有自己的分组,浏览器会为不同的任务组设置优先级。

2.微任务、宏任务

一个事件循环包含一个至多个任务队列,每个任务都有自己的分组,浏览器会为不同的任务组设置优先级。

宏任务macrotask

包含执行整体的JS代码(script标签内),事件回调,XHR回调,,IO操作,UI render、(setTimeout/setInterval/setImmediate)

可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。浏览器为了能够使得JS内部macrotask与DOM任务能够有序的执行,会在一个macrotask执行结束后,在下一个macrotask 执行开始前,对页面进行重新渲染,流程如下:

宏任务macrotask->渲染->宏任务macrotask->...

对于宏任务的执行顺序是这样的,那么微任务呢?

微任务microtask

更新应用程序状态的任务,主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)
可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前:

宏任务macrotask->微任务microtask(如果存在)->渲染->宏任务macrotask->微任务microtask...

在每一次执行完宏任务后,引擎立即会执行当前eventloop所有产生的微任务,也就是在视图更新之前(下一次宏任务之前)。

因此微任务Promise的执行顺序会早于宏任务setTimeout,当引擎遇到promise时,加入微任务,遇到settimeout加入宏任务,当前执行栈为空后,会执行所有此期间产生的微任务,微任务执行完毕后,渲染更新,然后执行宏任务。

关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

流程图如下:

3.async/await

上面一节的宏任务微任务分辨介绍了原理与Promise与定时器的执行顺序,这一节说说async/await。 当函数标记async时,就会标记为异步函数,进入异步任务队列。

Async/Await 其实是Generator函数的语法糖,也可以认为是一个自执行的generate函数

//Generator函数
function * testDat() {
    yield  1;
    yield  2;
}

let func = testDat();
func.next();     //1
func.next();      //2
复制代码

执行 Generator 函数会返回一个遍历器对象,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。通过next(),来时Generator函数内部的指针向下移动,也就是说,Generator 函数是分段执行的,yield表达式是暂停执行的标记

Async/Await:当遇到await关键字是,会等待执行await后边的语句,很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await紧跟的函数会先执行一遍,然后就会跳出整个async函数来执行后面调用栈的代码。等主线程宏任务执行完了(不包括微任务)之后,又会跳回到async函数中等待await后面表达式的返回值(promise)

await关键字后面函数的返回值是一个Promise对象,如果不是Promise则会隐式的转换为Promise:

//同步函数
const a = await 'hello world'
// 相当于
const a = await Promise.resolve('hello world');
复制代码

正因为此,引擎遇到await关键字后,会将紧跟await后的函数先执行一遍,然后await后面的代码加入到microtask中,然后就会跳出整个async函数

async function async1() {
	console.log('async1 start');
	await async2();
	console.log('async1 end');
}
//等价于
async function async1() {
	console.log('async1 start');
	Promise.resolve(async2()).then(() => {
                console.log('async1 end');
        })
}
复制代码

再次来看这道题

//标注执行顺序

/*
   async关键字标记函数内部可能存在异步操作,await关键字之前,代码会正常执行
*/
async function async1() {
    console.log('async1 start');//2.首先输出 async1 start
    
    await async2();  //进入async2();执行完成async2后,压入微任务队列(隐式Promise),并交出线程回到主线程
    
    console.log('async1 end');  // 6 (上文提到await后面的代码加入到microtask中)
                                // 根据压入微任务队列的顺序依次读取加入调用栈 输出async1 end
}
async function async2() {
 console.log('async2');  //3.打印 async2(记得上文说的,await后的函数会先执行一遍)
}

console.log('script start'); //1.函数开始执行输出 script start

//加入宏任务队列
setTimeout(function() {   
    console.log('setTimeout'); //8.执行宏任务队列 输出setTimeout
}, 0)

async1();  //2.执行

new Promise(function(resolve) {
    console.log('promise1');//4.Promise构造函数正常同步执行 输出promise1
    resolve();
}).then(function() { //返回Promise对象,压入微任务队列
    console.log('promise2'); // 7.根据压入微任务队列的顺序依次读取加入调用栈 输出promise2,至此微任务全部执行完毕,准备渲染->执行宏任务队列
});
console.log('script end');// 5. 输出script end,至此,执行栈全部执行完成,然后准备调用
                          //此次程序运行期间所有产生的微任务
复制代码

执行顺序如下:

  • 1.函数开始执行输出 script start
  • 2.遇到async1函数,进入执行首先输出 async1 start,
  • 3.遇到await async2();会先执行async2一次,输出async2,
    await后面的函数是一个Promise,所以加入微任务队列。并交出线程回到主线程
  • 4.Promise构造函数正常同步执行
    输出promise1,返回Promise,then()当中的代码压入微任务队列
  • 5.输出script end,至此,执行栈全部执行完成,然后准备调用此次程序运行期间所有产生的微任务
  • 6.输出async1 end
  • 7.根据压入微任务队列的顺序依次读取加入调用栈 输出promise2,至此微任务全部执行完毕,准备渲染->执行宏任务队列
  • 8.执行宏任务队列 输出setTimeout

*对于例子中的Promise操作全部都是 直接console输出,实际工作中例如ajax返回数据时间未知,那么按照上述逻辑假如async2函数需要10s才返回结果(pending状态),那么会不会影响(阻塞)下一个微任务 "console.log('promise2');"的执行呢,实际上:

promise.then 并不是立即注册了一个微任务,这有两种情况:

  • 如果当前的 promise 状态已经是 fulfilled 或者 rejected,那么promise.then 会立刻注册一个微任务
  • 如果当前的 promise 状态还是 pending, 那么promise.then 会把这个回调“存储起来”,等到该 promise 的状态改变再注册一个微任务。

转载于:https://juejin.im/post/5d3036205188252d1f28ec20

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值