从原理上弄懂async和await

162 阅读7分钟

从原理上弄懂async和await

1. 什么是async和await

async和await是js在ES8中引入的一种解决异步回调的方案, 将异步代码转为形式上的同步代码,增加代码的可读性,以及简化了代码,解决了回调地狱等问题.

2. async和await的基本使用

2.1. async

2.1.1

async的语法就是在函数前面加上async,被async修饰的函数,我们称之为异步函数,就会默认返回一个promise对象,这个对象要么是一个状态为fulfilled的promise对象, 要么是一个因为未被捕获的错误,状态为rejected的promise对象.这个promise对象值由你这个函数本身返回的值,或者抛出的错误决定.

// state为fulfilled,result为undefined
async function foo(){
    console.log("foo")
}

// state为fulfilled,result为1
async function foo(){
    return 1
}

// state由返回的promise对象决定
async function foo(){
    return new Promise((resolve,reject)=>{})
}

async function foo(){
    throw new Error("err")
}
复制代码
2.1.2 异步函数,它就是一定是异步执行吗?

实际上,如果里面没有特殊的代码, 异步函数的执行流程和普通函数是一样的.


console.log("main start")
async function foo(){
    console.log("foo start")
}
foo()
console.log("main end")
// 结果 main start , foo start , main end
复制代码

那它里面到底是怎么执行的,我们后面再说.

2.2 await

await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理 (fulfilled),其回调的 resolve 函数参数作为 await 表达式的值,继续执行 async function

若 Promise 处理异常 (rejected),await 表达式会把 Promise 的异常原因抛出。

另外,如果 await 操作符后的表达式的值不是一个 Promise,则返回该值本身。 ----引自MDN

3. 基本使用

3.1使用promise

function foo(n) {
    return new Promise((resolve, reject) => {
        if (n > 0) {
            resolve(n)
        } else {
            reject("出错了")
        }
    })
}

function bar() {
    foo(2)
        .then(res => {
            console.log(res)
        })
        .catch(err => {
            console.log(err)
        })
}
bar() // console.log(2)
复制代码

3.2 async/await

async function bar() {
    const res = await foo(2)
    console.log(res)
}
复制代码

从代码量,以及代码可读性来看,await确实更好.特别是当需要连续处理多个异步代码时, async/await的优势更加明显.

3.3 例子

将第一个异步请求,获取到的结果,作为第二个异步请求的参数拼接"bbb",将第二个请求获取到的结果,作为第三个异步请求的参数拼接"ccc",最后将第三次请求的结果返回.

//模拟请求
function request(url){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // 请求成功的回调
            if (url) {
                resolve(url)
            } else {
                // 请求失败的回调
                reject(new Error("request fail"))
            }
        }, 2000)
    })
}

// 使用promise 写法一
request("aaa").then(res => {
    request(res + "bbb").then(res => {
        request(res + "ccc").then(res => {
            console.log(res)
        })
    })
 })
 // 写法二:
 request("aaa")
    .then(res => {
        return request(res + "bbb")
    })
    .then(res => {
        return request(res + "ccc")
    })
    .then(res => {
        console.log(res)
    })
复制代码

无论是写法一还是写法二,都不够优雅.写法一容易出现回调地狱, 写法二的链式调用可读性不好.
这个时候我们的主角就出现了.

async function getData() {
    const res1 = await request("aaa")
    const res2 = await request(res1 + "bbb")
    const res3 = await request(res2 + "ccc")
    console.log(res3)
}
getData()
复制代码

通过async和await实现起来,确实更加方便,同时代码也更加优雅.
那么我们就来看看,async/await到底是怎么工作的.那这就不得不提到我们的Generator(生成器)和Iterator(迭代器)了.

4.生成器和迭代器

4.1 迭代器

在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。更具体地说,迭代器是通过使用 next() 方法实现 Iterator protocol 的任何一个对象,该方法返回具有两个属性的对象: value,这是序列中的 next 值;和 done ,如果已经迭代到序列中的最后一个值,则它为 true 。如果 value 和 done 一起存在,则它是迭代器的返回值。
简单的说,迭代器就是一个实现了next方法的对象.
-----引自MDN

4.2 生成器

生成器对象是由一个 generator function 返回的
同时,生成器也是一个特殊的迭代器.

//生成器函数
function* foo() {
    console.log("foo开始执行")
    const n1 = 100
    console.log(n1)
    const m1 = yield n1 * 100

    const n2 = 200
    console.log(n2)
    console.log(m1) // 9999
    yield n2 * 100

    const n3 = 200
    console.log(n3)
    yield n3 * 100
    
    console.log("foo执行结束")
}

//执行生成器函数,返回一个生成器对象
const generator = foo()

//通过调用生成器对象上的next方法,手动控制函数的执行流程,每调一次next方法, 函数就会执行一段代码,以yield作为分分界.同时,next中可以传递一个参数, 这个参数作为上一个yield的返回值.
// yield后面的表达的值作为next方法返回的对象中的value的值
console.log("返回值:", generator.next()) //返回值: { value: 10000, done: false }
console.log("返回值:", generator.next(9999)) //返回值: { value: 20000, done: false }
console.log("返回值:", generator.next()) //返回值: { value: 10000, done: false }
console.log("返回值:", generator.next()) //{ value: undefined, done: true 
复制代码

4.3 generator的使用

在简单了解了生成器的用法后, 我们使用生成器来实现一下上面的那个例子.这个地方是手动调用的, 也可以通过递归,判断done的值自动调用.这样写的目的,是为了更加方便理解每个next方法的执行时机.

function* getData() {
    const res1 = yield request("aaa")
    const res2 = yield request(res1 + "bbb")
    const res3 = yield request(res2 + "ccc")
    console.log(res3)
}
 generator.next().value.then(res => {
    generator.next(res).value.then(res => {
        generator.next(res).value.then(res => {
            generator.next(res)
        })
    })
})
复制代码

调用第一个next方法,返回值中的value为yield后面表达式的值, 所以,这个地方的value为一个promise对象,通过then方法,获取这个promise 的值, 作为参数,传递给next;

调用第二个next,执行函数中的第二段代码, 获取到第二个yield后面表达式的值,也是一个promise对象,通过then方法获取这个promise的值,然后将这个值作为参数传递给next;

调用第三个next,执行第三段代码,同上, 然后调用最后一个next方法,因为,它是最后一段代码了, 我们直接执行就ok了.

4.4 对比

看到这里,有没有发现使用async/await实现,和使用生成器实现的代码,有那么一丢丢相似呢.

function* getData() {
    const res1 = yield request("aaa")
    const res2 = yield request(res1 + "bbb")
    const res3 = yield request(res2 + "ccc")
    console.log(res3)
}
async function getData() {
    const res1 = await request("aaa")
    const res2 = await request(res1 + "bbb")
    const res3 = await request(res2 + "ccc")
    console.log(res3)
}
复制代码

我们不妨大胆的类比一下, 我们在使用async/await的时候,代码的实际执行流程,就是我们通过生成器实现的代码流程. 这也就是为啥我们会说, async/awiat是generator+yield+promise的语法糖.

5. async/aiwat在事件循环中角色

在了解了,async,await的实际执行流程以后,我们来看一道面试题.

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')
// script star, async1 start, async2, promise1, script, end, async1 end, promise2, setTimeout
复制代码

之前也能做出来,但是我在await async2()这个地方的代码状态,我一直都是很模糊的.现在来分析一下吧:

首先,执行主线程上的任务,打印了"script start";

然后,将setTimeout的回调函数放入宏任务队列中;

再然后,开始执行async1(),打印"async1 start",执行到await async2(),通过上面的学习,我们知道了,这里会先直接执行async2(),打印"async2",然后async2函数返回了一个状态为fulfilled,值为undefind的promise对象,即,await async2()后面的代码都在返回promise对象then方法的回调中执行,所以,console.log('async1 end')会被加入到微任务队列中;

然后,执行到new Promise(),打印出"promise1",并将then方法中的回调加入到微任务队列中;

最后打印出"script end",主线程代码执行完毕;

开始执行,微任务队列中的代码,即依次打印出"async1 end","promise2";

清空微任务队列以后,开始执行宏任务中的代码,打印"setTimeout".

以上就是,这个题的整个分析过程了,只有在完全了解,代码执行的流程,才会更加自信.

还要一道类似的,要复杂一点点的,用上述的分析,一样可以的.

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    new Promise(function (resolve) {
        console.log('promise1');
        resolve();
    }).then(function () {
        console.log('promise2');
    });
}
console.log('script start');
setTimeout(function () {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function (resolve) {
    console.log('promise3');
    resolve();
}).then(function () {
    console.log('promise4');
});
console.log('script end');
//script start, 
// async1 start, 
// promise1, 
// promise3, 
// script end, 
// promise2,
// async1 end,
// promise4, 
// setTimeout

感谢各位观看,希望能对您有帮助,有不足的的地方,欢迎大家指出交流.