看两道关于异步的字节面试题...

我们今天来讲解两道经典的字节面试题。在讲解之前,我们需要学习一个前置知识------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了。

相关推荐
cnsxjean1 小时前
Vue教程|搭建vue项目|Vue-CLI2.x 模板脚手架
javascript·vue.js·ui·前端框架·npm
web组态软件1 小时前
BY组态-低代码web可视化组件
前端·低代码
react_in1 小时前
webpack 题目
前端·webpack
MarisolHu1 小时前
前端学习笔记-Vue篇-02
前端·vue.js·笔记·学习
学前端的小朱1 小时前
Webpack的基础配置
前端·webpack·node.js
小小优化师 anny1 小时前
JS +CSS @keyframes fadeInUp 来定义载入动画
javascript·css·css3
小周同学_丶2 小时前
解决el-select数据量过大的3种方法
前端·vue.js·elementui
先知demons2 小时前
uniapp开发微信小程序笔记10-触底加载
前端·笔记·微信小程序·小程序·uni-app
每一天,每一步2 小时前
react antd不在form表单中提交表单数据,而是点查询按钮时才将form表单数据和其他查询条件一起触发一次查询,避免重复触发请求
前端·javascript·react.js
NoneCoder3 小时前
HTML5系列(9)-- Web Components
前端·html·html5