帮助你开始理解async/await

6,092 阅读5分钟

一边学习前端,一边通过博客的形式自己总结一些东西,当然也希望帮助一些和我一样开始学前端的小伙伴。

如果出现错误,请在评论中指出,我也好自己纠正自己的错误

author: thomaszhou

让我们开始学习async和await

async/await使用同步的思维,来解决异步的问题。

  • async的优点

    • 利用async创建的函数也是异步函数,就像setTimeout那种一样
    • async/await 的优势在于处理 then 链:
      • 如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了,因为promise参数传递太麻烦了,而async/await特别方便
      • async可以直接接收传递的变量,但是peomise的then是独立作用,如果要取值,就要将部分数据暴露在最外层,在 then 内部赋值一次.
  • 相比较generator

    • (1)内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行
    • (2)更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
    • (3)更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

同步和异步的理解:当我们发出了请求,并不会等待响应结果,而是会继续执行后面的代码,响应结果的处理在之后的事件循环中解决。那么同步的意思,就是等结果出来之后,代码才会继续往下执行。

我们可以用一个两人问答的场景来比喻异步与同步。A向B问了一个问题之后,不等待B的回答,接着问下一个问题,这是异步。A向B问了一个问题之后,然后就笑呵呵的等着B回答,B回答了之后他才会接着问下一个问题,这是同步。

1、安装支持

babel已经支持,所以我们可以在webpack中使用 首先在当前项目中使用npm下载babel-loader。

npm install babel-loader --save-dev

然后在配置文件webpack.confing.dev.js中配置在module.exports.module.rules中添加如下配置元素即可。

  {
    test: /\.(js|jsx)$/,
    include: paths.appSrc,
    loader: require.resolve('babel-loader'),
    options: {
      cacheDirectory: true,
    },
  },

如果你使用最新版本的create-react-app或者vue-cli来构建你的代码,那么它们应该已经支持了该配置。

2、普通声明和await使用

  • async函数实际上返回的是一个Promise对象
async function fn() {
    return 30;
}

// 或者
const fn = async () => {
    return 30;
}

在声明函数时,前面加上关键字async,这就是async的用法。当我们用console.log打印出上面声明的函数fn,我们可以看到如下结果:

console.log(fn());

//result
Promise = {
    __proto__: Promise,
    [[PromiseStatus]]: "resolved",
    [[PromiseValue]]: 30
}

很显然,fn的运行结果其实就是一个Promise对象。因此我们也可以使用then来处理后续逻辑。

fn().then(res => {
    console.log(res);  // 30
})

await的使用-------------------

await的含义为等待。就是代码需要等待await后面的函数运行完并且有了返回结果之后,才继续执行下面的代码。这正是同步的效果

但是我们需要注意的是,await关键字只能在async函数中使用。并且await后面的函数运行后必须返回一个Promise对象才能实现同步的效果

当我们使用一个变量去接收await的返回值时,如:const temp = await fn();该返回值temp为Promise中resolve出来的值(也就是PromiseValue)

// 定义一个返回Promise对象的函数
function fn() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(30);
        }, 1000);
    })
}

// 然后利用async/await来完成代码
const foo = async () => {
    const t = await fn(); // 将30传入
    console.log(t);
}

foo();
console.log('begin')

// begin
// 30

首先我们定义了一个函数fn(),这个函数返回Promise,并且会延时 1 秒,resolve并且传入值30,foo函数在定义时使用了关键字async,然后函数体中配合使用了await,最后执行foo()。整个程序会在 1 秒后输出30,也就是说foo()中常量t取得了fn()中resolve的值,并且通过await阻塞了后面的代码执行,直到fn()这个异步函数执行完。

运行这个例子我们可以看出,当在async函数中,运行遇到await时,就会等待await后面的函数运行完毕,而不会直接执行next code。

可以看到begin优先输出,是因为async/await创建的foo()函数也是异步函数,所以你懂的

如果我们直接使用promise的then方法的话,想要达到同样的结果,就不得不把后续的逻辑写在then方法中。

const foo = () => {
    return fn().then(t => {
        console.log(t);
        console.log('next code');    
    })
}
foo();

很显然如果使用async/await的话,代码结构会更加简洁,逻辑也更加清晰。

从代码片段中不难看出 Promise 没有解决好的事情,比如要有很多的 then 方法,整块代码会充满 Promise 的方法,而不是业务逻辑本身.

而且每一个 then 方法内部是一个独立的作用域,要是想共享数据,就要将部分数据暴露在最外层,在 then 内部赋值一次.

虽然如此,Promise 对于异步操作的封装还是非常不错的,所以 async/await 是基于 Promise 的,await 后面是要接收一个 Promise 实例。

3、异常处理

在Promise中,我们知道是通过catch的方式来捕获异常。而当我们使用async时,则通过try/catch来捕获异常。

function fn() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('some error.');
        }, 1000);
    })
}

const foo = async () => {
    try {
        await fn();
    } catch (e) {
        console.log(e);  // some error
    }
}

foo();

await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中

如果有多个await函数,那么只会返回第一个捕获到的异常

function fn1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('some error fn1.');// 设置reject
        }, 1000);
    })
}
function fn2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('some error fn2.'); // 设置reject
        }, 1000);
    })
}

const foo = async () => {
    try {
        await fn1();
        await fn2();
    } catch (e) {
        console.log(e);  // some error fn1.
    }
}

foo();
async

4、async/await 的优势在于处理 then 链

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

例子一:

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}
  • Promise 方式来实现这三个步骤的处理
function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

  • async/await 方式来实现这三个步骤的处理
async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样

例子二:

现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(m, n) {
    console.log(`step2 with ${m} and ${n}`);
    return takeLongTime(m + n);
}

function step3(k, m, n) {
    console.log(`step3 with ${k}, ${m} and ${n}`);
    return takeLongTime(k + m + n);
}
  • 用 async/await 来写:
async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => {
            return step2(time1, time2)
                .then(time3 => [time1, time2, time3]);
        })
        .then(times => {
            const [time1, time2, time3] = times;
            return step3(time1, time2, time3);
        })
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

有没有感觉有点复杂的样子?那一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,看着就晕!

5、await in for 循环

  • await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。(注意想forEach,map,reduce这种也是函数!!!!)
    • 正确的写法是采用 for 循环。!!!!
async function dbFuc(db) {
  let docs = [{}, {}, {}];

  // 报错
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}

上面代码会报错,因为 await 用在普通函数之中了。但是,如果将 forEach 方法的参数改成 async 函数,也有问题。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  // 可能得到错误结果
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
}

上面代码可能不会正常工作,原因是这时三个 db.post 操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用 for 循环

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);
  }
}
  • 如果确实希望多个请求并发执行,可以使用 Promise.all 方法
    • 先将多个函数(任务)都保存到doc这个数组中,就可以保存多个任务,然后再实现并发执行
async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

// 或者使用下面的写法

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  // 将多个函数(任务)都保存到doc这个数组中,就可以保存多个任务,然后再实现并发执行
  let promises = docs.map((doc) => db.post(doc));

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}

6、实践

在实践中我们遇到异步场景最多的就是接口请求,那么这里就以jquery中的$.get为例简单展示一下如何配合async/await来解决这个场景。

// 先定义接口请求的方法,由于jquery封装的几个请求方法都是返回Promise实例,因此可以直接使用await函数实现同步
const getUserInfo = () => $.get('xxxx/api/xx');

const clickHandler = async () => {
    try {
        const resp = await getUserInfo();
        // resp为接口返回内容,接下来利用它来处理对应的逻辑
        console.log(resp);

        // do something
    } catch (e) {
        // 处理错误逻辑
    }
}

7、一个问题测试

题目

可修改下面的 aa() 函数,目的是在一秒后用 console.log() 输出 want-value

function aa() {
    setTimeout(function() {
        return "want-value";
    }, 1000);
}
  • 但是,有额外要求:
    • aa() 函数可以随意修改,但是不能有 console.log()
    • 执行 console.log() 语句里不能有 setTimeout 包裹

解答

问题的主要目的是考察对异步调用执行结果的处理,既然是异步调用,那么不可能同步等待异步结果,结果一定是异步的

setTimeout() 经常用来模拟异步操作。最早,异步是通过回调来通知(调用)处理程序处理结果的

function aa() {
    return new Promise((resolve) => {
      setTimeout(function() {
        resolve('want-value');
			},1000);
		});
  }

async function fn() {
    let temp = await aa();
    console.log(temp);
}
fn();

参考文章: