先来看一段代码:
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,await,Promise这些的概念。
async
MDN上是这样描述async的:
async 函数中可能会有 await 表达式,这会使 async 函数暂停执行,等待表达式中的 Promise 解析完成后继续执行 async 函数并返回解决结果。
阮一峰老师的解释通俗一点:
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
所以MDN描述的暂停执行,实际上是让出了线程(跳出async函数体)然后继续执行后面的脚本的。
Promise
Promise是异步编程的一种解决方案,它有三种状态,分别是pending-进行中、resolved-已完成、rejected-已失败。当Promise的状态由pending转变为resolved或rejected时,会执行相应的方法,并且状态一旦改变,就无法再次改变状态,这也是它名字promise-承诺的由来。在ES6中,Promise终于成为了原生对象,可以直接使用。
let promise = new Promise ( (resolve, reject) => { if ( success ) { resolve(a) // pending ——> resolved 参数将传递给对应的回调方法 } else { reject(err) // pending ——> rejectd } })
Promise.prototype.then(),then()是Promise原型链上的方法,它包含两个参数方法,分别是已成功resolved的回调和已失败rejected的回调:
promise.then( () => { console.log('this is success callback') }, () => { console.log('this is fail callback') } )
Promise.prototype.catch(),.catch()的作用是捕获Promise的错误,与then()的rejected回调作用几乎一致。但是由于Promise的抛错具有冒泡性质,能够不断传递,这样就能够在下一个catch()中统一处理这些错误。同时catch()也能够捕获then()中抛出的错误,所以建议不要使用then()的rejected回调,而是统一使用catch()来处理错误。
promise.then( () => { console.log('this is success callback') } ).catch( (err) => { console.log(err) } )
then() 和 catch() 都会返回一个新的Promise对象,可以链式调用:
promise.then(() => { console.log('this is success callback') }) .catch((err) => { console.log(err) }) .then(...) .catch(...)
await
语法:[return_value] = await expression;
表达式(express):一个 Promise 对象或者任何要等待的值。
返回值(return_value):返回 Promise 对象的处理结果。如果等待的不是 Promise 对象,则返回该值本身。
所以,当await操作符后面的表达式是一个Promise的时候,它的返回值,实际上就是Promise的回调函数resolve的参数。我们都知道Promise是一个立即执行函数,但是他的成功(或失败:reject)的回调函数resolve却是一个异步执行的回调。当执行到resolve()时,这个任务会被放入到回调队列中,等待调用栈有空闲时事件循环再来取走它。
setTimeout、setInterval
setTimeout、setInterval都是宏任务,是在最后执行的。setTimeout是在多少时间间隔后把事件扔进队列里,不是马上执行。比如setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。setInterval是间隔多少时间就把事件扔进队列一次。
宏任务一般是:包括整体代码script,setTimeout,setInterval。
微任务:Promise,process.nextTick。
题目分析
铺垫完这些概念,我们回过头看上面那道题目。一开始只是两个函数的定义,不执行。接着是第一个console.log,执行直接输出script start
。然后遇到了setTimeout,不管,先扔在后面。
然后是async1(),执行async1 这个函数,首先会打印出async1 start
。然后执行到 await async2(),发现 async2 也是个 async 定义的函数,所以直接执行了“console.log(‘async2’)”,输出async2
。同时async2返回了一个Promise(这里阮一峰老师的解释我觉得更容易理解:async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句),划重点:此时返回的Promise会被放入到回调队列中等待,await会让出线程(js是单线程),接下来就会跳出 async1函数 继续往下执行。
然后执行到 new Promise,promise是立即执行的,所以先打印出来promise1
,然后执行到 resolve 的时候,resolve这个任务就被放到回调队列中等待,然后跳出Promise继续往下执行,输出script end
。
接下来同步的事件都循环执行完了,调用栈现在已经空出来了,那么事件循环就会去回调队列里面取任务继续放到调用栈里面了。这时候取到的第一个任务,就是前面 async1里 执行到 await async2()时async2放进去的Promise,输出async1 end
。
接下来取到的下一个任务,就是前面 new Promise 放进去的 resolve回调,也就是那个.then()里的操作,输出promise2
。
调用栈再次空出来了,事件循环就取到了下一个任务,终于轮到的那个async2放进去的Promise的resolve回调!!执行它什么也不会打印的,因为 async2 并没有return东西。此时 await 定义的这个 Promise 已经执行完并且返回了结果。最后别忘了!还有一个我们扔在后面不管的setTimeout,输出setTimeout
。
所以正确答案就是:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout