6

====据说这是今日头条去年的一道笔试题,主要考察的是setTimeout async promise执行顺序

~先双手奉上这道题目~

   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');
 
  • 首先,我们先来了解几个概念:

    JS众所周知是单线程语言,Javascript引擎同一时刻只能执行一个代码块,使用Event Loop作为它的异步执行机制

  • 那么Event Loop是如何实现异步呢,个人浅显的理解如下:

    1. 同步代码按照上下文的顺序放进主进程中去执行
    2. 异步函数放进异步队列中,等待执行,在异步队列执行的顺序按照先进先出的原则
    3. 等主进程中的同步函数执行完毕后,轮询去执行异步队列中的异步函数

      ⚠️注意: setTimeOut并不是直接的把你的回掉函数放进上述的异步队列中去,而是在定时器的时间到了之后,把回掉函数放到执行异步队列中去。如果此时这个队列已经有很多任务了,那就排在他们的后面。这也就解释了为什么setTimeOut为什么不能精准的执行的问题了。setTimeOut执行需要满足两个条件:

      1. 主进程必须是空闲的状态,如果到时间了,主进程不空闲也不会执行你的回掉函数 
      2. 这个回掉函数需要等到插入异步队列时前面的异步函数都执行完了,才会执行
      
  • 理解了Eventloop异步实现的方式,再来补充一下promise、async/await

    1. 首先,new Promise是同步的任务,会被放到主进程中去立即执行。而.then()函数是异步任务会放到异步队列中去,那什么时候放到异步队列中去呢?当你的promise状态结束的时候,就会立即放进异步队列中去了。如果你要问他和setTimeOut谁当进去的快,要从下面两个方面考虑:

      1. promise结束时。.then内函数插入异步队列的时间与setTimeOut的回掉函数插入队列的时间,谁的早,谁的就最快
      2. **如果promise是同步的而setTimeOut时间是0,那么是promise先执行**。至于什么,查了很多的资料,了解到:一个浏览器环境只能有一个事件循环,而一个事件循环可以有多个任务队列。settimeout所在的队列与promise.then()的队列不同,面对此种情况,v8实现的时候会先从promise.then()的队列取任务,但是并没有很理解,如果有大佬愿意指点迷津,请留言告知?
    2. 带async关键字的函数会返回一个promise对象,如果里面没有await,执行起来等同于普通函数;如果没有await,async函数并没有很厉害是不是
    3. await 关键字要在 async 关键字函数的内部,await 写在外面会报错;await如同他的语意,就是在等待,等待右侧的表达式完成。此时的await会让出线程,阻塞async内后续的代码,先去执行async外的代码。等外面的同步代码执行完毕,才会执行里面的后续代码。就算await的不是promise对象,是一个同步函数,也会等这样操作

接下来,带着上面的那些总结,步入正题

分析上述的代码,给任务类型分类

async function async1() {
    console.log( 'async1 start’ ) // 同步代码2
    await async2()  // 执行async2
    console.log( 'async1 end’ )   // 需要等async1外面的代码执行完,并且async2也执行完才会执行
}
async function async2() {
    console.log( 'async2’ )  // 同步代码3
}

// ===================== 从这里开始了表演,所以在这里走起 ===============================

console.log( 'script start’ )    // 同步代码1

setTimeout( function () {
 //  setTimeout放入event-loop中的macro-tasks队列,暂不执行
console.log( 'setTimeout' )
}, 0 )

async1()  //  如果有await,第一个【await前面的代码】属于主进程执行—看最上面函数内分析

new Promise( function ( resolve ) {  // 注意这个方法里面的是同步
             // 同步代码4
    console.log( 'promise1’ ) 
    resolve();
} ).then(  
    //  .then()放入event-loop中的micro-tasks队列
     function () {
        console.log( 'promise2' )
    } 
)

console.log( 'script end’ )  // 同步代码5
  1. 最先输出的是同步代码,按照上下文执行顺序是

    'script start’
    'async1 start’
    'async2’ 
    'promise1’ 
    'script end’
  2. 接下来,给异步队列执行的排序

    第一个执行的函数是async1() ,里面有await,他要等待,等待就是要让async外面的代码先执行,外面的那些同步代码执行好了之后,在执行这个async里面的,await后面的代码执行。所以遇到await就让出了线程,给到后面的代码,那么下面的promise.then会先执行,后执行await后面的代码
    async1()的await后面和setTimeOut哪个先执行,这个要看await等待的是什么。此处等待的是async2函数,这个函数里面没有await等待,带async关键字的函数返回的是一个promise对象,所以async1中等待的是一个promise对象,并且这个promise对象里面只有同步执行,会被放进时间循环的micro-tasks队列,该队列比setTimeOut的队列先执行,执行完了之后还不能执行setTimeOut,因为执行完了之后就不阻塞主进程了,主进程要接着执行;也就是await后面的同步代码,要先去执行主进程。所以setTimeOut是在最后执行的
    再次验证了上面所说的setTimeOut执行的必要条件之一是主进程空闲了
    因此,异步代码的执行顺序是

‘promise2'        // async函数外的代码先执行
'async1 end’    // -- await不阻塞了,async后面的同步代码
'setTimeout'   // promise.then的队列比setTimeout队列先执行

如果你真正的理解了Event Loop的执行机制,并且知道setTimeout的与promise.then并非一个队列里面的,那么这道题就是很简单的送分题

上述如有不足或者不当指出,请各位大佬匡正~~?


南山
30 声望0 粉丝