一道面试题
代码如下:
代码语言:javascript复制let a;
const b = new Promise((resolve,reject)=>{
console.log('promise')
resolve()
}).then(()=>{
console.log('promise2')
}).then(()=>{
console.log('promise3')
})..then(()=>{
console.log('promise4')
})
a = new Promise((resolve,reject)=>{
console.log(a)
await b;
console.log('after1')
await a;
resolve(true)
console.log('after2')
})
console.log('end')
问: 最终打印出什么结果?
我原本想的答案是下面的结果:
代码语言:javascript复制//1. end
//2. promise
//3. type error
理由是,这能看出来道题其实考察的是下面的知识点:
- 变量声明
- promise 消息队列或者叫(微任务和宏任务)
- async 和 await 的用法
let 声明的变量存在TMD
暂时性死区的问题,所以已声明但未被赋值的变量如果直接使用,会报未定义的错。
我把这个代码执行了一遍,确实报错了,但是报的是这个:
代码语言:javascript复制Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules
原来是我少写了个async
,真实的问题应该是这样:
let a;
const b = new Promise((resolve,reject)=>{
console.log('promise')
resolve()
}).then(()=>{
console.log('promise2')
}).then(()=>{
console.log('promise3')
})..then(()=>{
console.log('promise4')
})
a = new Promise(async (resolve,reject)=>{
console.log(a)
await b;
console.log('after1')
await a;
resolve(true)
console.log('after2')
})
console.log('end')
这次的结果是:
代码语言:javascript复制// promise
// undefined
// end
// promise2
// promise3
// promise4
// after1
思考一下什么为会出现这种结果?其实还是上面说的那三个知识点。但是要是真正理解上面的三个知识点,又需要理解下面的知识点:
JS的并发模型和事件循环
JavaScript 有个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务,这个模型与其他语言的模型截然不同。
上图中有三个部分:栈内存
,堆内存
,和消息队列
。这是js的一个基本模型。
栈内存有两个作用:1,存放基本类型数据。2.提供代码运行环境。
堆内存的作用:主要是存储引用类型的数据。
消息队列:一个JavaScript运行时包含了一个带处理消息的消息队列。每个消息都关联一个用于处理这个消息的回调函数。
在事件循环期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移除队列,并作为输入参数来调用与之关联的函数。
函数的处理会一直进行到执行栈再次为空为止,然后事件循环队列会处理队列中的下一个消息。
这里有个问题,消息是什么?
个人理解消息就是事件的回调函数。
在浏览器里,每当一个事件发生并且有一个事件监听器绑定在该事件上时,一个消息就会被添加进消息队列。如果没有事件监听器,这个事件将会丢失。所以当一个带有点击事件处理器的元素被点击时,就会像其他事件一样产生一个类似的消息。
函数 setTimeout 接受两个参数:待加入队列的消息和一个时间值(可选,默认为 0)。这个时间值代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其它消息并且栈为空,在这段延迟时间过去之后,消息会被马上处理。但是,如果有其它消息,setTimeout 消息必须等待其它消息处理完。因此第二个参数仅仅表示最少延迟时间,而非确切的等待时间。比如:
代码语言:javascript复制const s = new Date().getSeconds();
setTimeout(function() {
// 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
console.log("Ran after " (new Date().getSeconds() - s) " seconds");
}, 500);
while(true) {
if(new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}
// Good, looped for 2 seconds
// Ran after 2 seconds
可以看到 执行代码后先输出了 Good, looped for 2 seconds
,然后输出Ran after 2 seconds
。
因为代码执行到setTimeout发现它是一个消息,将它加入到了消息队列中,等到栈清空以后,才又接着处理这个消息。
宏任务和微任务的概念
Here “platform code”
means engine, environment, and promise implementation code.
下面这个表格可以很清楚的描述宏任务和微任务的概念:
宏任务 | 微任务 | |
---|---|---|
谁发起 | 宿主环境(Node,浏览器) | 平台引擎 |
具体事件 | 1. script (可以理解为外层同步代码)/n2. setTimeout/setInterval、3. UI rendering/UI事件,4. postMessage,MessageChannel,5. setImmediate,I/O(Node.js) | promise,MutationObserver,process.nextTick |
运行顺序 | 在后 | 在前 |
触发新一轮tick | 会 | 不会 |
async 和 await
async
关键字加到函数申明中,可以告诉我们返回的是 promise,而不是直接返回值。以往我们写promise
的时候,需要在then
的返回值中才能捕获我们想要的结果。
但是await
可以直接捕获我们想要的结果。
简单来说:await
关键字使JavaScript运行时暂停于此行,允许其他代码在此期间执行,直到异步函数调用返回其结果。一旦完成,我们的代码将继续从下一行开始执行。
比如:
代码语言:javascript复制async getDataList=()=>{
const data = await getOtherList()
return data.blob()
}
解析器会在此行上暂停,直到当服务器返回的响应变得可用时。此时 getOtherList()
返回的 promise 将会完成(fullfilled),返回的 response 会被赋值给 response 变量。一旦服务器返回的响应可用,解析器就会移动到下一行,从而创建一个Blob
。Blob这行也调用基于异步promise的方法,因此我们也在此处使用await。当操作结果返回时,我们将它从getDataList()
函数中返回。
那么又出现一个问题
Blob
是什么?