换个角度:setTimeout跟promise跟promise.then以及async跟await中代码的执行先后顺序为?
说白了:就是promise还有generator跟async三者的用法

问题等价于理解消息队列与事件循环
前言
待添加
整个串起来
Philip搞了一个的碉堡的工具来可视化这个过程,这玩意儿叫Loupe。这是一个能够把JavaScript运行时可视化的工具。
我们用它来看一个简单的例子:在一个异步的setTimeout
回调中用console.log
在控制台打些log出来。
整个过程到底都发生了什么呢?我们来看一下:
- 执行进入
console.log('Hi');
函数,因此这个函数被丢进了调用栈里。 console.log('Hi');
函数return了,因此他就被弹出了栈顶。- 执行进入
setTimeout
函数,因此这个函数被丢进了调用栈里。 setTimeout
是Web APIs
的一部分,因此Web APIs
处理了他,并且等了2秒- 继续执行脚本,进入
console.log('EvenyBody')
函数,把他也丢进调用栈。 console.log('EvenyBody')
函数return了,所以把他从栈顶弹出去- 2秒的定时已经完成了,所以就把对应的回调函数放到回调队列里。
- 事件循环检查调用栈是否为空,如果非空的话,他就等着。因为调用栈现在是空的,所以把回调队列中的回调函数丢进调用栈。
console.log('There')
函数返回了,因此把他从栈顶弹出去(译者按:原文为console.log(‘Everybody’),应为书写错误)。
有趣的一点是:setTimeout(function(...), 0)
的情况。setTimeout
为0的时候这个过程看起来可能不明显,除非考虑到调用栈的执行环境和事件循环的情况。基本上都会推迟到调用栈为空才执行。
宏任务和微任务执行完成后都会判断是否还有微任务,有的话执行微任务,没有就执行宏任务,如此循坏
面试题实战
promise
1 | new Promise(resolve => { |
这道题的输出是123,为什么不是132呢?因为我一直理解Promise是没有异步功能,它只是帮忙解决异步回调的问题,实质上是和回调是一样的,所以如果按照这个想法,resolve之后应该会立刻then。但实际上并不是。难道用了setTimeout?
如果在promise里面再加一个promise:
1 | new Promise(resolve => { |
执行顺序是1243,第二个Promise的顺序会比第一个的早,所以直观来看也是比较奇怪,这是为什么呢?
- 不哔哔,搞段代码瞅瞅:
1 | setTimeout(function() { |
- 首先会遇到setTimeout,将其放到宏任务event queue里面
- 然后回到 promise , new promise 会立即执行, then会分发到微任务
- 遇到 console 立即执行
- 整体宏任务执行完成,接下来判断是否有微任务
,刚刚放到微任务里面的then,执行 - ok,第一轮事件结束,进行第二轮,刚刚我们放在event queue 的setTimeout 函数进入到宏任务,立即执行
- 结束
再来两道题看看:
1 | new Promise((resolve, reject) => { |
1 | console.log('script start') |
1 | console.log('script start'); |
script start
script end
promise1
promise2
setTimeout
再来一道涉及冒泡的题:
1 | // Let's get hold of those elements |
click
promise
mutate
click
promise
mutate
timeout
timeout
终于结束了,我们来贴段巨复杂的代码搞一搞
process.nextTick(callback)类似node.js版的”setTimeout”,在事件循环的下一次循环中调用 callback 回调函数。
除了广义的同步任务和异步任务,我们可以分的更加精细一点:
- microtasks:
setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks:process.nextTick, Promise, MutationObserver
不同的任务会进入到不同的event queue。比如setTimeout和setInterval会进入相同的Event Queue。
1 | console.log('1'); |
惊不惊喜,意不意外,我们来分析一下
- 首先先执行console.log(1) 然后将setTimeout放到宏任务event queue里面 记作 setTimeout 1 ,接着 看到 process.nextTick ,将其放到微任务里面 ,记作 process 1,然后 看到new promise 立即执行输出9 ,将里面的then 放到 微任务里面 记作 then 2, 继续,遇到 setTimeout 放到宏任务里面记作 setTimeout 2 。目前输出的是:1,7,
- OK, 接下来,开始判断是否有微任务,刚刚放入到微任务event queue的进入到主程序开始执行,process 1 , then 2 目前输出的是:6,8、
- 接下来,微任务的event queue 空了,进行下一轮事件,将刚刚放到宏任务的 setTimeout 1 进入到主线程
遇到 console 立即执行, 遇到 process.nextTick 放到微任务 event queue 里面 记作 process1, 接着遇到 new Promise 立即执行, 将 then 放到event queue 里面 记作 then 2,OK,当前宏任务里的任务执行完了,判断是否有微任务,发现有 process1, then 2 两个微任务 , 一次执行 目前输出的是:2,4,3,5、 - 目前主线程里的任务都执行结束了,又开始第三轮事件循环,同上(字太多,省略。。。。) 目前输出的是:9,11,10,12、
注意: 以上所说只能是在浏览器中的执行顺序,
async
1 | async function asyncFn() { |
这样就表示这是异步函数,返回的结果
返回的是一个promise
对象,状态为resolved
,参数是return
的值。那再看下面这个函数
1 | async function asyncFn() { |
async 表示函数里有异步操作
await 表示紧跟在后面的表达式需要等待结果
还有一点,在async
里,必须要将结果return
回来,不然的话不管是执行reject
还是resolved
的值都为undefine
,建议使用箭头函数。
其余返回结果都是判定resolved
成功执行。
1 | //正确reject方法。必须将reject状态return出去。 |
最后一行Promise { <resolved> : undefined }
是因为返回的是console.log
执行语句,没有返回值。
控制台直接运行代码,会返回最后一句代码的返回值
await
await
意思是async wait(异步等待)。这个关键字只能在使用async
定义的函数里面使用。任何async
函数都会默认返回promise
,并且这个promise
解析的值都将会是这个函数的返回值,而async
函数必须等到内部所有的 await
命令的 Promise
对象执行完,才会发生状态改变。
打个比方,await是学生,async是校车,必须等人齐了再开车。
就是说,必须等所有await
函数执行完毕后,才会告诉promise
我成功了还是失败了,执行then
或者catch
很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的函数会先执行一遍(比如await Fn()的Fn ,并非是下一行代码),然后就会跳出整个async函数来执行后面js栈的代码。等本轮事件循环执行完了之后又会跳回到async函数中等待await后面表达式的返回值,如果返回值为非promise则继续执行async函数后面的代码,否则将返回的promise放入Promise队列(Promise的Job Queue)
来看个简单点的例子
1 | const timeoutFn = function(timeout){ |
这里本可以用箭头函数写方便点,但是为了便于阅读本质,还是换成了ES5写法,上面执行函数内所有的await函数才会返回状态,结果是执行完毕3秒后才会弹出’完成
‘。
正常情况下,await 命令后面跟着的是 Promise ,如果不是的话,也会被转换成一个 立即 resolve 的 Promise。
也可以这么写
1 | function timeout(time){ |
这样用then链式回调的方式执行resolve
1 | //打印结果 |
用async/await呢?
1 | async function start() { |
达到了相同的效果。但是这样遇到一个问题,如果await
执行遇到报错呢
async
里如果有多个await函数的时候,如果其中任一一个抛出异常或者报错了,都会导致函数停止执行,直接reject
;
怎么处理呢,可以用try/catch
,遇到函数的时候,可以将错误抛出,并且继续往下执行。
1 | let last; |
练习:
1 | function testSometing() { |
执行结果
我们一步步来解析
首先test()
打印出test start...
然后 testFn1 = await testSomething();
的时候,会先执行testSometing()
这个函数打印出“testSometing
”的字符串。
之后因为await
会让出线程就会去执行后面的。testAsync()
执行完毕返回resolve
,触发promiseFn
打印出“promise START...
”。
接下来会把返回的Promiseresolve("promise RESOLVE")
放入Promise队列(Promise的Job Queue),继续执行打印“===END===
”。
等本轮事件循环执行结束后,又会跳回到async
函数中(test()
函数),等待之前await
后面表达式的返回值,因为testSometing()
不是async
函数,所以返回的是一个字符串“return`
testSometing`”。
test()
函数继续执行,执行到testFn2()
,再次跳出test()
函数,打印出“testAsync
”,此时事件循环就到了Promise的队列,执行promiseFn.then((val)=> console.log(val));
打印出“promise RESOLVE
”。
之后和前面一样 又跳回到test函数继续执行console.log(testFn2)
的返回值,打印出“hello async
”。
最后打印“test end...
”。
加点料,让testSomething()
变成async
1 | async function testSometing() { |
执行结果:
和上一个例子比较发现promiseFn.then((val)=> console.log(val));
先于console.log(testFn1)
执行。
原因是因为现在的版本async函数会被await resolve,
testSomething()
已经是async函数,返回的是一个Promise对象需要等它 resolve 后将当前Promise 推入队列,随后先清空调用栈,所以会”跳出” test() 函数执行后续代码,随后才开始执行该 Promise。
【题目】
1 | async function async1(){ |
答案:
1 | script start |
事实上,没有在控制台执行打印之前,我觉得它应该是这样输出的:
1 | script start |
为什么这样认为呢?因为我们(粗浅地)知道await之后的语句会等await表达式中的函数执行完得到结果后,才会继续执行。
MDN是这样描述await的:
async 函数中可能会有 await 表达式,这会使 async 函数暂停执行,等待表达式中的 Promise 解析完成后继续执行 async 函数并返回解决结果。
会认为输出结果是以上的样子,是因为没有真正理解这句话的含义。
阮一峰老师的解释我觉得更容易理解:
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
对啦就是这样,MDN描述的暂停执行,实际上是让出了线程(跳出async函数体)然后继续执行后面的脚本的。这样一来我们就明白了,所以我们再看看上面那道题,按照这样描述那么他的输出结果就应该是:
1 | script start |
关于await前后可以理解成一个 new promise的过程
1 | // 原来 async 内部的代码 |
同步的请求、页面上会变成静止状态、也就是拿不到响应之前、不允许用户操作其他东西
async+await其实还是异步、
这就变成同步和异步的区别了。。。
举个例子、
相当于你是老板、同步就是你自己去买菜了、买了菜回来、才可以开会之类的做公司的事情
异步就相当于、你一边在网上买菜一边工作、网上的菜什么时候送来是根据当时的路况和其他因素、你该开会开会、该工作工作、不会被阻塞