其一
群里看到的一道事件循环的题:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
setTimeout(() => {
console.log('timer1')
}, 0)
}
async function async2() {
setTimeout(() => {
console.log('timer2')
}, 0)
console.log("async2");
}
async1();
setTimeout(() => {
console.log('timer3')
}, 0)
console.log("start")
看到这里,你可以猜想一下输出结果是什么 …
正确答案应该是:
async1 start
async2
start
async1 end
timer2
timer3
timer1
第一次做的时候我其实猜错了,我以为答案应该是:
async1 start
async2
async1 end
start
timer2
timer1
timer3
这里的关键其实是搞清楚 await async2()
做了什么事情。我以为在 async1
内部,async2
被调用之后,就会继续往后执行,因此是先打印 async1 end
,再回到主栈打印 start
。然而 async2
里面包含了一个异步操作,在异步操作得到结果之前,其实是会跳出当前 async1
函数的执行栈,优先去执行同步任务的,所以这里其实会先执行 start
,再去执行 async1 end
。具体地说:
async function f(){
await p1
// some operations
}
// 可以近似理解为
function f(){
return Promise.resolve(p1).then(res => {
//some operations
})
}
所以原来的代码可以近似理解为:
async function async1() {
console.log("async1 start");
return Promise.resolve(async2()).then(res => {
console.log("async1 end");
setTimeout(() => {
console.log('timer1')
},0)
})
}
async function async2() {
setTimeout(() => {
console.log('timer2')
}, 0)
console.log("async2");
}
async1();
setTimeout(() => {
console.log('timer3')
}, 0)
console.log("start")
接下来就可以像以前分析事件循环那样去理解了:
第一轮事件循环:
① 宏任务:整个代码块作为宏任务执行,调用 async1
函数,进入函数执行栈。打印 async1 start
;async2
位于 Promise 的执行器中,因此立即执行,遇到了定时器 timer2
,把其回调函数分发到宏任务队列,之后打印 async2
;Promise 的 then
的回调函数被分发到微任务队列。 async1
执行完毕,返回到主栈,遇到了定时器 timer3
,其回调函数被分发到宏任务队列。接着打印 start
,主栈清空。
② 微任务:微任务队列中有 then
的回调函数,进入主栈并执行,打印 async1 end
,之后遇到定时器 timer1
,其回调函数注册到被分发到宏任务队列。之后,微任务队列中无任务,第二轮事件循环结束
第二轮事件循环:
① 宏任务:根据之前进队列的顺序,宏任务队列中依次有 timer2
、timer3
和 timer1
这几个定时器的回调函数。timer2
的回调函数进入主栈并执行,打印 timer2
② 微任务:微任务队列中无任务,第二轮事件循环结束
第三轮事件循环:
① 宏任务:宏任务队列中依次有 timer3
和 timer1
这两个定时器的回调函数。timer3
的回调函数进入主栈并执行,打印 timer3
② 微任务:微任务队列中无任务,第三轮事件循环结束
第四轮事件循环:
① 宏任务:宏任务队列中只有 timer1
这个定时器的回调函数。timer1
的回调函数进入主栈并执行,打印 timer1
② 微任务:微任务队列中无任务,第四轮事件循环结束
其二
之后又看到这么一段代码(Nodejs):
let fs = require('fs')
function readFile(fileName) {
return new Promise(function (resolve,reject) {
fs.readFile(fileName,function (error,data) {
if(error) return reject(error)
resolve(data)
})
})
}
async function readAll(paths){
const promises = paths.map(async path => {
const res = await readFile(path)
return res.toString()
})
for(promise of promises){
console.log(4)
console.log(await promise)
}
}
readAll(['1.txt','2.txt','3.txt'])
这段代码异步读取本地文件,假设三个 txt 的内容分别是 123,最后会输出什么呢?
答案是 4 1 4 2 4 3
。确实是道很简单的题 … 问的不过是 async
和 await
的常规用法,而且语义上相比 Promise 已经清晰不少了。不过我的老毛病又犯了,想着想着就绕了进去:既然 for...of
是个同步操作,打印 4
的时候不应该能拿到异步读取文件的结果,所以我以为应该是输出:
4
Promise <pending>
4
Promise <pending>
4
Promise <pending>
不过从实际结果来看,却很像是同步打印 4
和文件内容,为什么会这样呢? —— 其实只是看起来像而已!毕竟 async await
所做的就是让我们用同步的方式编写异步代码,但其实,在第一次打印 4
之后,往后的打印操作其实是被放在一个异步的回调里面的。
这么说可能不清楚,不妨仿照第一道题的方式,看能不能用同样的方法分析这段代码。前面说过,await
后面的操作可以放在一个 then
的回调里,所以可以把 readAll
函数近似改写为:
async function readAll(paths){
const promises = paths.map(async path => {
return Promise(readFile(path)).then(res => {
return res.toString()
})
})
console.log(4)
console.log(await promise[0])
console.log(4)
console.log(await promise[1])
console.log(4)
console.log(await promise[2])
}
}
再进一步改写为:
async function readAll(paths){
const promises = paths.map(async path => {
return Promise(readFile(path)).then(res => {
return res.toString()
})
})
console.log(4)
return Promise.resolve(promise[0]).then(res => {
console.log(res)
console.log(4)
return Promise.resolve(promise[1]).then(res => {
console.log(res)
console.log(4)
return Promise.resolve(promise[2]).then(res => {
console.log(res)
})
})
})
}
}
这是按照前面规则改写的结果,但是好像又退回了 callback hell
的恶心形式,所以这里要再改为 Promise 的链式调用:
async function readAll(paths){
const promises = paths.map(async path => {
return Promise.resolve(readFile(path)).then(res => {
const r = res
return r.toString()
})
})
console.log(4)
return Promise.resolve(promises[0]).then(res => {
console.log(res)
console.log(4)
return Promise.resolve(promises[1])
}).then(res => {
console.log(res)
console.log(4)
return Promise.resolve(promises[2])
}).then(res => {
console.log(res)
})
}
然后我们再按照事件循环去分析:
第一轮事件循环:
① 宏任务: 整个代码块作为宏任务执行,调用 readAll
函数,进入函数执行栈;通过 map
迭代数组,每一次迭代会立即执行 Promise 中的执行器,进而执行 readFile
函数,由于 resolve
是位于异步回调函数中(尚未执行),所以这里返回的是一个处于 pending
状态的 Promise;此外,每一次迭代也会将 then
中的回调分发到微任务队列。在回调执行前,我们是拿不到文件内容的。 readAll
执行完毕,返回到主栈,第一次打印 4
。接着遇到了 Promise.resolve(promises[0])
,这里实际上也是一个处于 pending
状态的 Promise,调用它的 then
方法的时候,会把回调分发到微任务队列。这里有三个 then
,每一个 then
里的回调都会依次进入微任务队列。
② 微任务:现在的微任务队列中其实有 6 个任务,依次是返回文件内容的三个回调函数,以及上面所说的三个 then
的回调。从队头任务开始,6 个任务依次进入主栈并顺序执行。这里就会发现,前三个任务的执行负责返回三个文件的内容,后三个任务的执行,每次都会打印一个文件的内容和一个 4
。所以我们看到的打印就是:
4
1
4
2
4
3
看起来就很像是同步打印 4
和文件内容,不过其实里面做的仍然是异步操作,这就是 async await
一个强大的地方了。
当然,这里即便用前面带有嵌套回调的代码来分析,也会得出相同的结果,其实这里要保证的主要就是进微任务队列的顺序。不过,为什么一开始会猜想出错误的结果呢?= = 其实是因为忘记对 for...of
中的 await
进行转化(被自己菜哭)。
如果非要得到那个错误的结果的话,代码其实是这样的:
async function readAll(paths){
const promises = paths.map(async path => {
return Promise.resolve(readFile(path)).then(res => {
const r = res
return r.toString()
})
})
for(promise of promises){
console.log(4)
console.log(promise)
}
}
注意这里的 promise
前面没有加上 await
,所以 for...of
里面的代码就是彻底的同步代码了,在每一次打印 4
之后,promise
也是紧跟着打印出来的,经过前面的分析可以知道,这个时候的 promise
还处于 pending
状态,所以打印结果自然会是:
4
Promise <pending>
4
Promise <pending>
4
Promise <pending>
其实本身是简单的代码,非要往复杂的方向想、来回绕,反而浪费了不少时间。回想起来,在以前的学习中也做过不少这种钻牛角尖的事情,看来今后还是应该尽量去克服这个毛病。不过在思考这两段代码的时候,感觉慢慢地也体会到了书里所说的一些东西,这应该也算是一个小小的收获吧。