【经典面试题系列】下面代码的输出是什么?

今天给大家带来一道出现频率很高的面试题,最初是在头条的面试中出现,主要考察大家对 event loopPromiseasync/await 等知识点的掌握情况。

题目

请写出下面代码的输出结果

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')

千万不要看到 async 就害怕,其实它并不神秘。

前置知识点

在解析这道题目之前,希望大家能先理解以下几个知识点:

event loop的执行顺序:

  • 一开始整个脚本作为一个宏任务执行
  • 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  • 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完
  • 执行浏览器UI线程的渲染工作
  • 检查是否有Web Worker任务,有则执行
  • 执行完本轮的宏任务,回到第2步,依此循环,直到宏任务和微任务队列都为空

宏任务和微任务

  • 微任务包括:MutationObserverPromise.then()catch()Promise为基础开发的其它技术,比如fetch APIV8的垃圾回收过程、Node独有的process.nextTick
  • 宏任务包括:scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

注意:在所有任务开始的时候,由于宏任务中包括了script,所以浏览器会先执行一个宏任务,在这个过程中你看到的延迟任务(例如setTimeout)将被放到下一轮宏任务中来执行。

async/await

在这里也不赘述 async/await 的基础内容,大家需要知道:

  • async 声明的函数,其返回值必定是 promise 对象,如果没有显式返回 promise 对象,也会用 Promise.resolve() 对结果进行包装,保证返回值为 promise 类型
  • await 会先执行其右侧表达逻辑(从右向左执行),并让出主线程,跳出 async 函数,而去继续执行 async 函数外的同步代码
  • 如果 await 右侧表达逻辑是个 promise,让出主线程,继续执行 async 函数外的同步代码,等待同步任务结束后,且该 promise 被 resolve 时,继续执行 await 后面的逻辑
  • 如果 await 右侧表达逻辑不是 promise 类型,那么仍然异步处理,将其理解包装为 promise, async 函数之外的同步代码执行完毕之后,会回到 async 函数内部,继续执行 await 之后的逻辑

题目分析

基于以上知识点,我们来进行题目的分析:

  • 首先执行同步代码,输出 script start,并向下执行,遇见 setTimeout,将其回调放入宏任务当中
  • 继续执行同步代码逻辑,遇见 async1(),执行 async1 内同步代码,输出 async1 start,继续下后执行到 await async2(),执行 async2 函数
  • async2 函数内并没有 await,按顺序执行,同步输出 async2,按照 async 函数规则,async2 函数仍然返回一个 promise,作为 async1 函数中的 await 表达式的值。相当于:Promise.resolve().then(() => {})
  • 同时 async1 函数让出主线程,中断在 await 一行。
  • 回到 async1 函数外,继续执行,输出 Promise 构造函数内 promise1,同时将这个 promise 的执行完成逻辑放到微任务当中
  • 执行完最后一行代码,输出 script end
  • 此时同步代码全部执行完毕,回到 async1 函数中断处,优先执行微任务
  • Promise.resolve().then(() => {}) 其实什么也没做。但这时候 await 中断失效,继续执行 async1 函数,输出 async1 end
  • 这时候检查微任务,输出 promise2
  • 这时候微任务全部执行完毕,检查宏任务,输出 setTimeout

我将代码重新拷贝,加上注释,我们再来回顾一下:

async function async1({
   console.log('async1 start'// step 4: 直接打印同步代码 async1 start
   await async2() // step 5: 遇见 await,首先执行其右侧逻辑,并在这里中断 async1 函数
   console.log('async1 end'// step 11: 再次回到 async1 函数,await 中断过后,打印代码 async1 end
}

async function async2({
   console.log('async2'// step 6: 直接打印同步代码 async2,并返回一个 resolve 值为 undefined 的 promise
}

console.log('script start'// step 1: 直接打印同步代码 script start

// step 2: 将 setTimeout 回调放到宏任务中,此时 macroTasks: [setTimeout]
setTimeout(function({            
   console.log('setTimeout'//step 13: 开始执行宏任务,输出 setTimeout
}, 0)  

async1() // step 3: 执行 async1

// step 7: async1 函数已经中断,继续执行到这里
new Promise(function(resolve{
   console.log('promise1'// step 8: 直接打印同步代码 promise1
   resolve()
}).then(function(// step 9: 将 then 逻辑放到微任务当中
   console.log('promise2'// step 12: 开始执行微任务,输出 promise2
})

console.log('script end'// step 10: 直接打印同步代码 script end,并回到 async1 函数中继续执行

最后的输出顺序是:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

PS:这道题最后的async1 endpromise2 的顺序其实存在争议,与浏览器实现或者Node.js的版本有关。

总结

由上述例题可见,这一类面试常见的「必考题」灵活多变,且会受到语言规范以及浏览器实现的影响。虽然有些考察点「涉嫌」「刁难」面试者,但是掌握最基本的异步理论、清楚规范要求细节,确实是能够灵活运用的关键,也是能够避免或追查 bugs 的必备知识。

我对大家的建议是,对于这些内容不必头大,见一个分析一个,分析一个就「死记」一个,规范永远没有为什么,但是仔细思考却总有它的道理。不然你们想想,JavaScript 为什么一开始就是单线程异步的?

最后

《前端面试题宝典》经过近一年的迭代,现已推出 小程序 和 电脑版刷题网站 (https://fe.ecool.fun/),欢迎大家使用~

同时,我们还推出了面试辅导的增值服务,可以为大家提供 “简历指导” 和 “模拟面试” 服务,现在参与还有额外优惠,感兴趣的同学可以联系小助手(微信号:interview-fe)进行体验哦~