我们今天来讲解两道经典的字节面试题。在讲解之前,我们需要学习一个前置知识------async和await的用法。
1. async和await的用法
async和await是用来解决代码异步问题的。我们上一次讲过解决异步的方法可以用回调函数和Promise。而async和await就是和Promise息息相关的东西。
我们来回顾一下异步是一个什么概念。
为什么会出现异步呢?因为JS是单线程语言,它一次性只能干一件事。而代码有耗时代码和不耗时代码之分,当v8引擎读到耗时代码时,它会先将其挂起,执行后面不耗时的代码,再回过头来执行耗时代码。但在很多情况下,耗时代码就是要先执行,比如封装一个向后端发送http请求接收数据的接口时,要先把数据拿到手再去对其进行操作。所以我们要去解决代码异步执行的问题。我们可以使用Promise来解决异步。我们来回顾一下它的用法。
比如说我们有一个函数a,我们依旧使用定时器来模拟耗时的代码。
js
function a() {
setTimeout(() => {
console.log('a');
}, 1000)
}
还有一个不耗时的函数b。
js
function b() {
console.log('b');
}
如果我们先调用b再调用a,那就会先输出b再输出a。如果我们先调用a再调用b,那也会先输出b再输出a。因为函数a需要耗时执行,v8引擎不会优先执行函数a,它会先去执行函数b再来执行函数a。
那我们说过可以使用Promise解决异步问题,它的语法是怎么使用来着。
我们在函数a里返回一个Promise实例对象, Promise接收一个回调函数,有两个参数resolve和reject。我们把函数a中的代码放到这个函数中去。
js
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('a');
resolve()
}, 1000)
})
}
还记得我们说过的resolve和reject有什么用吗?它们都是两个函数,它们就像两个开关。可以让我们人为的去设置这个实例对象的状态。调用resolve,就会变成成功状态,就去执行then里面的代码;调用reject,就会变成失败状态,就去执行catch里面的代码。
这里我们调用了resolve,于是在后面我们可以这样写:
js
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('a');
resolve()
}, 1000)
})
}
function b() {
console.log('b');
}
a()
.then(() => {
b()
})
a调用then方法,接收一个回调函数,将b的调用放到这个函数里面。如果函数resolve有返回值,就会被这个then函数接收到,当然这里没有。这样我们就解决了代码的异步执行问题。
那回到我们今天的主题,我们还可以用async和await解决异步。我们来看一下:
同样是这份代码,我们来学习一下它的语法:
js
function a() {
setTimeout(() => {
console.log('a');
}, 1000)
}
function b() {
console.log('b');
}
首先我们人为的去写一个函数fn,然后在这个函数的前面加一个关键字async,然后在这个函数里面去调用a和b,在a的前面加一个关键字await。然后调用这个函数fn。
js
// async await
function a() {
setTimeout(() => {
console.log('a');
}, 1000)
}
function b() {
console.log('b');
}
async function fn() {
await a()
b()
}
fn()
这样就行了吗?还不够,因为await只能操作Promise实例对象,所以我们还是需要在函数a里面return一个new Promise。
js
// async await
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('a');
resolve()
}, 1000)
})
}
function b() {
console.log('b');
}
async function fn() {
await a()
b()
}
fn()
这样才是正确的语法。
成功的解决了异步。其实就是将then那里改成了async和await。
这个async是干嘛用的呢?我们用它去定义了一个函数,我把这个函数输出给你看一下。
js
async function fn() {
}
console.log(fn());
我们发现得到的是一个Promise对象。所以这个关键字写在哪个函数前面,就调用这个函数return了一个new Promise。
那既然如此,你看看我们能不能这样写:
js
// async await
async function a() {
setTimeout(() => {
console.log('a');
}, 1000)
}
function b() {
console.log('b');
}
await a()
b()
因为async能返回一个Promise实例对象,那我拿它代替函数里面的return new Promise
行不行。然后在全局await a()。我们运行一下看看。
报错了,它显示await只能在async声明的函数中使用,那我再修改一下:
js
// async await
async function a() {
setTimeout(() => {
console.log('a');
}, 1000)
}
function b() {
console.log('b');
}
async function fn() {
await a()
b()
}
fn()
那我们就再写一个函数fn,用async定义了它,然后在里面去await a()。这样行吗?
b先执行了a再执行,说明没生效。我们一看,我们async得到的这个对象是不是没调用resolve啊,说明它的状态我们没设置,那我们这样写:
js
async function a() {
setTimeout(() => {
console.log('a');
Promise.resolve()
}, 1000)
}
function b() {
console.log('b');
}
async function fn() {
await a()
b()
}
fn()
Promise.resolve() 能得到一个状态为成功的实例对象,这样行不行呢?
也不行。因为这样我们是得到了两个实例对象,我们只是将里面那个Promise对象的状态改为了成功,外面这个async得到的Promise还是无状态。
这说明async和await还是不能脱离Promise去使用,它只是改变了then的调用。
所以我们可以下结论:
async 添加在函数声明之前,相当于在函数中返回了一个没有状态的 Promise的实例对象
await 后面接的是异步,必须是一个 Promise的实例对象,await 会将后面的代码的执行结果返回出来,并且它只能在async函数中使用
Promise.resolve() 会得到一个状态变更为成功的 promise对象
还有一个小细节,当我们使用then和catch的写法,我们会用catch来接收错误。那async和await写法怎么接收错误呢?
js
function getData() {
return 'Data'
}
async function foo() { // 声明一个可以调用异步的函数
try {
await getData()
} catch (error) {
console.log(error); // error就是try中的错误
}
}
我们假设getData是一段耗时代码,我们使用try/catch来接收错误。这并不是专门为async/await打造的方法,而是JS中本来就有这种处理错误的方法,只不过我们拿到这里来使用了。
2.字节面试题
了解完了这些,我们就能来看一看这道面试题了:
js
function getJson() {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log(2);
resolve(2)
}, 2000)
})
}
// 将 async,await 翻译成 promise
async function testAsync() {
await getJson()
console.log(3);
}
testAsync()
定义一个函数 getJson,用定时器模拟了耗时代码,在定时器里面输出2,resolve出来了一个2。然后async声明一个函数testAsync,里面await getJson()再输出3。
输出结果应该是先输出2再输出3,我们已经讲过很多次了。他会问你的是请将async,await 翻译成 promise。就是说我要用Promise的写法代替掉async,await应该怎么写呢?
这样可能有点难理解,我们先来举个例子看看,再来看这道题:
js
function getData() {
return 1
}
async function getAsyncData() {
return 1
}
async function getPromise() {
return new Promise(function (resolve, reject) {
resolve(1)
})
}
async function test() {
let a = 2
let c = 1
await getData()
let d = 3
await getPromise()
let e = 4
await getAsyncData()
return 2
}
我们来看看这份代码会被v8引擎执行成什么样。getData不用翻译,我们来翻译一下getAsyncData。
js
async function getAsyncData() {
return 1
}
我们上面说了。async会返回一个没有状态的Promise实例对象,那我们怎么把它翻译成Promise呢?
我们这样写:
js
function getAsyncData() {
return Promise.resolve().then(() => {
return 1
})
}
Promise.resolve() 可以得到一个状态为成功的Promise实例对象,而只有在状态为成功的Promise实例对象后面才能接then方法,而then方法自己也能返回一个Promise实例对象,因为then后面还能接then,而then方法返回的是一个没有状态的Promise实例对象,不是刚好符合async的特征吗?所以我们可以将async翻译成这样。这就是题目的意思。
我们使用Promise.resolve() 只是为了后面能接then。
再来,对于getPromise,我们也来翻译一下。
js
async function getPromise() {
return new Promise(function (resolve, reject) {
resolve(1)
})
}
async返回一个没有状态的Promise实例对象,我们直接在getPromise这样写:
js
function getPromise() {
return Promise.resolve().then(() => {
return new Promise(function (resolve, reject) {
resolve(1)
})
})
}
return Promise.resolve().then(() => {} 就相当于async的作用。
再来看这个,它会被执行成什么样呢?
js
async function test() {
let a = 2
let c = 1
await getData()
let d = 3
await getPromise()
let e = 4
await getAsyncData()
return 2
}
首先async不要,我们已经知道它翻译成什么样子了。然后读到await getData(),是不是要等这行代码执行完毕后才会执行下面的代码,所以这里是不是相当于要在第一个then后面接一个then,把后面的代码放到then里面去,等getData执行出结果了,第一个then执行完了,才执行第二个then里面的代码,因为 await 会将后面的代码的执行结果返回出来,我们还要在这里return getData()。
js
function test() {
return Promise.resolve()
.then(() => {
let a = 2
let c = 1
return getData()
})
.then(() => {
})
}
然后将let d = 3
放到这个then里面去。然后又读到await getPromise(),只有等这个出结果了,后面的代码才能执行,所以还要接一个then。
js
function test() {
return Promise.resolve()
.then(() => {
let a = 2
let c = 1
return getData()
})
.then(() => {
let d = 3
return getPromise()
})
.then(() => {
})
}
然后同样,将let e = 4
放到这个then里去,读到await getAsyncData()
再接一个then。
js
function test() {
return Promise.resolve()
.then(() => {
let a = 2
let c = 1
return getData()
})
.then(() => {
let d = 3
return getPromise()
})
.then(() => {
let e = 4
return getAsyncData()
})
.then(() => {
return 2
})
}
这就是最后翻译出来的结果,最终代码:
js
function getData() {
return 1
}
function getAsyncData() {
return Promise.resolve().then(() => {
return 1
})
}
function getPromise() {
return Promise.resolve().then(() => {
return new Promise(function (resolve, reject) {
resolve(1)
})
})
}
function test() {
return Promise.resolve()
.then(() => {
let a = 2
let c = 1
return getData()
})
.then(() => {
let d = 3
return getPromise()
})
.then(() => {
let e = 4
return getAsyncData()
})
.then(() => {
return 2
})
}
理解了这些,我们再来看这道面试题:
js
function getJson() {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log(2);
resolve(2)
}, 2000)
})
}
// 将 async,await 翻译成 promise
async function testAsync() {
await getJson()
console.log(3);
}
testAsync()
现在你应该能翻译了。先来对async:
js
function getJson() {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log(2);
resolve(2)
}, 2000)
})
}
// 将 async,await 翻译成 promise
function testAsync() {
return Promise.resolve().then(() => {
return getJson()
})
}
testAsync()
然后读到await getJson()
,必须等这行代码出结果了,才执行后面的代码。所以在第一个then后面要再接一个then,将后续代码放到这个then中去。
js
function getJson() {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log(2);
resolve(2)
}, 2000)
})
}
// 将 async,await 翻译成 promise
function testAsync() {
return Promise.resolve()
.then(() => {
return getJson()
})
.then(() => {
console.log(3);
})
}
testAsync()
还有个小细节,我们发现getJson还resolve出来了一个2,那这个2会被谁接收到呢?
我们知道,then方法是可以接收参数的,res。这个res是不是这个then前面那个then返回出来的。所以第一个then返回出来了一个2,被它后面的then接收,所以这个应该写在第二个then里面。
js
function getJson() {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log(2);
resolve(2)
}, 2000)
})
}
// 将 async,await 翻译成 promise
function testAsync() {
return Promise.resolve()
.then(() => {
return getJson()
})
.then((2) => {
console.log(3);
})
}
testAsync()
这就是这道题目的正解。
3. 红绿灯问题
我们来看字节的第二道面试题:红绿灯问题。
就是有这样一个需求:用代码实现一个红绿灯效果:红灯每隔3秒钟亮一次,绿灯每隔1秒亮一次,黄灯每隔2秒亮一次。
这并不是再说红灯要亮多久,而是1秒过去了,绿灯亮一下,又过了1秒,黄灯亮一下,又过了1秒,红灯亮一下...不断交替的重复执行。你会有疑问了:1秒过去了,绿灯亮了,又过了1秒,不应该还是绿灯亮吗?所以我们只能让它亮一种颜色,此时就只能黄灯亮了,绿灯就不能亮。
应该怎么写呢?
js
function red() { // 每隔 3 秒
console.log('红');
}
function green() { // 每隔 1 秒
console.log('绿');
}
function yellow() { // 每隔 2 秒
console.log('黄');
}
我们定义一个函数light,接收两个参数,timer:亮多久;cb:回调。
js
function red() { // 每隔 3 秒
console.log('红');
}
function green() { // 每隔 1 秒
console.log('绿');
}
function yellow() { // 每隔 2 秒
console.log('黄');
}
let light = function (timer, cb) {
return new Promise((resolve, reject) => {
setTimeout(() => {
cb()
resolve()
}, timer)
})
}
这就是我们写的一个亮灯函数,我们直接把间隔的时间和灯的函数传进来,它就帮我们把灯的函数触发掉,也就是实现亮灯的效果。因为要耗时执行,所以我们要用Promise将定时器包裹起来。
这样我们依次去调用每个灯它就会绿灯亮,黄灯亮,红灯亮。但它不会往复执行。
我们想让它往复执行,这样写:
js
function red() { // 每隔 3 秒
console.log('红');
}
function green() { // 每隔 1 秒
console.log('绿');
}
function yellow() { // 每隔 2 秒
console.log('黄');
}
let light = function (timer, cb) {
return new Promise((resolve, reject) => {
setTimeout(() => {
cb()
resolve()
}, timer)
})
}
let step = function () {
Promise.resolve()
.then(() => {
return light(3000, red)
})
.then(() => {
return light(1000, green)
})
.then(() => {
return light(2000, yellow)
})
.then(() => {
step()
})
}
step()
我们再定义一个step函数,里面定义一个Promise.resolve(),这是一个状态为成功的Promise实例对象,我们在它后面接then,then里面的代码就能执行,然后我们写return light(3000, red),就去调用light函数,light(3000, red)又能返回一个状态为成功的Promise实例对象,因为在light里面我们调用了resolve,于是我们又能接then,在里面return light(1000, green),它又会返回一个状态为成功的Promise实例对象,再接一个then,里面return light(2000, yellow)。这样我们就让灯交替着亮了。
然后我们想让它重复执行,执行一样的操作,是不是要用递归啊,于是我们还能接一个then,里面调用自己,于是它就能反复执行亮灯的操作。
它就一直执行下去。
那把它翻译成async和await应该怎么写呢?
js
function red() { // 每隔 3 秒
console.log('红');
}
function green() { // 每隔 1 秒
console.log('绿');
}
function yellow() { // 每隔 2 秒
console.log('黄');
}
let light = function (timer, cb) {
return new Promise((resolve, reject) => {
setTimeout(() => {
cb()
resolve()
}, timer)
})
}
let step = async function () {
await light(3000, red)
await light(1000, green)
await light(2000, yellow)
step()
}
step()
是不是这样写就行了,async声明一个函数,在这个函数里面await调用light,就行了。是不是用async和await更简洁一点。
4. async和await的应用场景
看了这么多,你可能还会有点疑惑,在实际开发中我们是怎么使用async和await的呢?我们来写个小demo来运用一下它。
我们来写一个向后端请求数据的接口,就不写html了,直接写js。
html
<body>
<script>
function getData() {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://mock.mengxuegu.com/mock/66585c4db462b81cb3916d3e/songer/songer#!method=get', true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
}
}
</script>
</body>
这段代码我们上次已经写过了,就不做过多解释了,就是向后端发送请求要求数据。
这次我们这样写,封装一个函数,把getData打造成一个专门向后端发送请求的函数,只要你传进来一个url,我就能向这个地址发送请求。
html
<body>
<script>
function getData(url) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
}
}
let data = getData('https://mock.mengxuegu.com/mock/66585c4db462b81cb3916d3e/songer/songer#!method=get')
console.log(data);
</script>
</body>
但这时是有问题的,代码异步执行了嘛,data拿不到数据,所以我们可以用async/await来解决异步。
我们先用async声明一个函数,然后将异步代码放到这个函数中执行,当然在getData函数中我们还是需要return 一个 new Promise。
html
<body>
<script>
function getData(url) {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
// console.log(xhr.responseText);
resolve(xhr.responseText)
}
}
})
}
async function foo() {
let data = await getData('https://mock.mengxuegu.com/mock/66585c4db462b81cb3916d3e/songer/songer#!method=get')
console.log(data);
}
foo()
</script>
</body>
我们将获取到的xhr.responseText resolve出来,它就会被await返回出来,await可以说相当于then嘛。采用then写法这个res就会被getData后面的then接收到,这里就会被await接收到,然后返回出来。await能返回后面代码的执行结果。
我们看看有没有数据:
确实拿到了数据,这就是我们在实际开发中最常用的手法,省的再去写then了。