ES6之async/await

1.什么是async/await,相比与原生promise有什么优势?

阮一峰老师的ES6里这一章一开始就有这么一句话,用于解释async/await

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

我觉得这句话,对于我这种菜鸟,还是不能一看就过,需要好好了解一下具体是个什么意思。

 ‘async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,await命令就是内部then命令的语法糖

async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

    上面这个例子是书上给出的一个async的实例,书上说async是多个异步操作的包装成的promise对象,那么如果我们不用async,会是怎么样的呢?

    所以现在,我们来吧这个例子转化为不用async的模式,如下。显而易见,如果我们不实用async/await单纯使用promise,多次异步操作不能够像同步代码那样垂直,顺序书写。第二次以及多次的异步操作需要放到then后的回调函数内进行操作。这里还只有两次异步,如果有多次相关联(下一次异步操作需要上一次异步的结果)的异步操作的话,即便是promise也会显得很凌乱。

    而我们使用async/await如上面代码那样,我们只需在“外包装”的函数(即getStockPriceByName)前添加async标志,表示该函数里执行的是异步操作;然后在异步操作前添加await,把得到的结果给第二个异步操作即可。这样的书写看上去和同步代码一模一样,清晰又明了,但是它却是实实在在的异步操作,这样各位体会到async和await的好处了嘛?

function getStockPriceByName(name){
    return new Promise(function(resolve, reject){
        getStockSymbol(name);//第一次异步操作
        if(操作成功){
            resolve(data);
        } else {
            reject(err);
        }
    })
}
getStockPriceByName('apple').then(function(data){
    getStockPrice(data);//第二次异步操作
}).catch(function(err){
    console.log(err);
})

我还记得,我第一次看这些东西的时候,一直有很多疑问,相信很多小菜鸟们一开始也会这样的问题吧:

第一.如果async/await方法很简便,为什么在promise里面不能这么写,非得在then里面写下一个异步操作?

Answer:以我的看法,async/await的便捷之处只针对连环/相关联异步操作的这种情况。因为下一步的异步需要上一步异步的结果作为参数才能继续进行,而promise异步操作的结果只能在then的回调函数里面得到!这才是关键!想想,我们以前不用promise的时候,获取异步的结果是不是也是在回调函数里面。

什么?你说能不能不在回调里获取异步操作的结果,而像下面这样写?

这里用react里面的this.setState()举例,因为它也是个异步操作,同时也是用之前的那个例子举例比较麻烦

let num = 0;
this.setState({
    num: 1
})
console.log(this.num);

猜猜看,console出来的是0还是1?答案是0。为什么呢?这个涉及到JavaScript的执行机制EventLoop,大概意思是这样的:

  • 引擎首先判断JS代码是同步还是异步,同步就进入主线程,异步就进入event table

  • 异步任务在event table中注册函数,当满足触发条件后,被推入event queue

  • 同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主线程中

所以,即便物理位置异步代码在同步代码之前,也是会先执行同步代码,再执行异步代码的!

回到刚才那个例子,你明白为什么是0了嘛?因为同步代码先被执行,此时异步操作setState还在事件队列里面等待执行,所以num的值还是之前的0。那如果我们想要console出异步操作之后num的值怎么办呢?那就是想下面这样写在回调函数里面,它表示异步操作完成之后我们再执行console这步命令。

let num = 0;
this.setState({
    num: 1
},()=>{
    console.log(this.num);
)

现在你应该终于明白了为什么我们需要回调函数了吧?那么你也应该明白了为什么纯用promise实现多次相关联异步操作,每次都需要写在then的回调函数里面了吧,仅仅是因为我们在then里面才能取到上一步的结果而已。

所以async/await的伟大之处在于让我们能够像同步代码那样取书写异步代码(它没有使用回调函数哦!),这都是await的功劳!那么聪明如你也可以猜到await的用法或者叫目的是什么了吧-------它就是另外一种回调函数写法!!它会等待上一次异步执行完毕之后再执行下一个异步。

再次体会了阮一峰老师的这句await命令就是内部then命令的语法糖’ 含义

 

第二.如果async/await的作用是让异步代码像同步代码,那为什么不直接用同步代码算了,异步不就是解决js单线程代码阻塞的问题的吗?现在又改成非得等上一个执行完毕才能执行下一个,那意义何在?

Answer:之前看了一篇blog写的蛮清楚的https://blog.csdn.net/zy444263/article/details/84346276 大家可以看看。

回答这个问题之前,我们先要明白同步代码和异步代码执行上的差异,同步代码是百分百的单线程,也就是前一个执行完了才能执行下一个;但是异步代码并不是这样的,如果多个不关联的异步,他们是可以并发执行的,当然之前的那个例子是个例外,它是和关联的异步操作,这个例外不仅需要同步和异步之间的非阻塞执行方式,同时还需要多个相关联异步之间的阻塞执行方式,这也是为啥我们需async/await。

所以,如果我们把这种相关联的异步写成同步,虽然异步操作他们自己之间是阻塞执行了,但是它却阻塞了后面的同步操作,这不是我们希望看到的。

其次,await的阻塞方式并不是阻塞同步代码所在的主线程,await其实是阻塞的当前异步函数的异步线程。

总结

  1. 虽然await会阻塞async异步函数,但是并没有阻塞同步代码的主线程,同步和异步之间仍然相互不阻塞。
  2. 虽然await阻塞异步函数向后执行,看起来像是同步的,但是它本质还是异步的,我们同样可以并行执行其他的不关联的异步操作,而同步函数不能并行执行。

 

2.async/await用法

2.1 async返回的是一个Promise对象

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});

还是之前那个例子,我们可以看到代码把获取股票价格封装成了一个统一的方法,方便调用。这里要注意一点,即便async/await再怎么像同步函数,它还是个异步并返回一个promise。在调用它的得到最后的结果的时候还是要用then方法。

当然了,如果就是任性,就是不想用then回调也是可以的,反正在函数里面已经得到了stockPrice了,在外面像这样简单调用一下并且把stockPrice改成全局的变量,也是可以得到的。但是不推荐。

getStockPriceByName();

2.2 async函数返回的是 Promise 对象可以作为await命令的参数

再看一个例子,指定多少毫秒后输出一个值。

第一个函数timeout里面返回了一个promise对象,promise对象里面是一个异步操作setTimeout;第二个函数是个async函数,里面的await用于等待第一个函数执行完后再执行后面的同步操作console.log;最后再全局调用这个asyncPrint函数。

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world', 50);

×

因为async返回的promise可以作为await命令的参数,所以上面代码还可以写成下面这样:

async function timeout(ms) {
  await new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world', 50);

命令行运行结果如下,3秒后打印出hello world。

如果说之前的例子充分表明了async/await在异步操作之间的用处的话,这个例子则表现了它在异步和同步之间的用处------asyncPrint函数里面的同步操作console.log必须等待之前的await执行完之后才能被执行,这样就达到了x ms后输出值的效果。

 

到这里我又有两个疑问

第一. 之前说到await的阻塞方式并不是阻塞同步代码所在的主线程,await其实是阻塞的当前异步函数的异步线程,为什么这里又阻塞同步了呢?

Answser:之前所说的await不会阻塞同步代码的主线程指的是async函数之外的同步代码,而不是async函数内部的同步代码。的理解是这样的,如果函数前面已经加了async这个标志了,那么这个函数里面有一套自己的规则,所有带有await的语句都会阻塞之后代码的执行,无论同步还是异步。

可能这么说会有点绕,简而言之就是在async语句里,同步和await异步代码不会以Eventloop事件方式进行区分(也就不存在先执行同步 后执行异步的操作),带有await的异步和同步代码统一被视为同步代码,更准确的说是以同步式阻塞方式 从上至下依次执行

当然,如果异步操作不带await,那和一般的代码无异。如下代码所示,我们把异步前面的await去掉在执行,得到的结果是立即打印出hello world。

如上代码所示,我们把异步前面的await去掉在执行,得到的结果是先输出hello world

总之,在async函数内部,不管同步还是异步都要遵循这个规则-----就是必须等到await执行完之后再执行。

 

第二. 那如果async里面一些语句不带await呢?如果这些语句有的是同步的有的是异步的又是什么情况呢?

1.await语句前后都是同步代码,结果是先打印出helloworld,然后间隔3s在打印出aaa。

不带await则还是以老规则,先同步再异步。

2.下面两个例子充分体现了 ‘await并不是阻塞主线程执行,因为async函数本身是异步的,所以await其实是阻塞的当前异步函数的异步线程’  这句话的含义

第一个例子和第二个例子的唯一区别在于,第一个例子timeout1之前加了await,而第二个例子没有。你可以猜猜看两个代码的结果有什么不一样?

代码一 --------------> 结果是3s后打印出 i am timeout-1 再过3s才打印出i am timeout-2

代码二 --------------> 结果是3s后同时打印出i am timeout -1和i am timeout-2

想想看这是为什么? 因为await它会阻塞当前异步函数的异步进程 === 语句await timeout1阻塞了当前异步函数timeout1的进程 === 阻塞了异步函数timeout2的执行 === 先执行timeout1,完成之后再执行timeout2 === 3s后输出 i am timeout-1,再3s后输出 i am timeout-2

而第二个函数,两个异步函数timeout1和timeout2都没有带await,所以两个异步函数是并发执行的!也就是我们前面所说的,多个不关联的异步操作可以并发执行。

3.思路再打开一点,如果我们让timeout1后执行,timeout2先执行,但是timeout1前面带一个await会怎么样呢?

结果是3s后同时打印出,不过2在前1在后。因为await 放在后面,所以不存在timeout1阻塞timeout2的情况。函数一进来就会开始执行timeout2。又因为timeout2没有带await,所以他不会阻塞后面的代码的运行,且timeout1是异步代码,所以timeout1也会在同一时刻和timeout2并发执行,得到后面的结果。

3. async 函数有多种使用形式

// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭头函数
const foo = async () => {};

4.语法

4.1

async函数返回一个 Promise 对象。

async函数内部return语句返回的值,会成为then方法回调函数的参数。

async function f() {
  return 'hello world';
}

f().then(v => console.log(v))
// "hello world"

4. 2

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

return以后的代码不会被执行。

4.3

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

async function f() {
  await Promise.reject('出错了');
  await Promise.resolve('hello world'); // 不会执行
}

防止出错的方法,也是将其放在try...catch代码块之中。

async function f() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('出错了');
    });
  } catch(e) {
  }
  return await('hello world');
}

或者在调用的时候用catch捕获

async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error('出错了');
  });
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))

4.4

多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

let foo = await getFoo();
let bar = await getBar();

上面代码中,getFoogetBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。

// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

4.5 await命令只能用在async函数之中,如果用在普通函数,就会报错。

 

×

  • 15
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
promise和async/await都是用于处理异步操作的方式,但它们在语法和使用上有一些区别。 1. Promise:Promise是ES6引入的一种处理异步操作的方式。它是一个对象,表示一个异步操作的最终完成(或失败)及其结果的值。Promise提供了一个链式的调用方式,可以通过then()方法来处理异步操作的结果,也可以通过catch()方法来处理异常情况。 2. async/awaitasync/await是ES8引入的一种处理异步操作的方式。它是基于Promise的语法糖,使得异步代码看起来更像同步代码。使用async关键字声明一个函数为异步函数,然后在需要等待异步操作结果的地方使用await关键字来等待Promise对象的完成。 区别: - 语法简洁性:async/await相对于Promise更加简洁,更接近于同步代码的写法,使得代码更易读、易理解。 - 错误处理:使用Promise时需要通过catch()方法来捕获异常,而async/await可以使用try/catch语句来处理异常,使得错误处理更加直观。 - 链式调用:Promise通过链式调用的方式来处理多个异步操作,而async/await可以使用同步的方式编写多个异步操作,使得逻辑更加清晰。 - 可读性:async/await相对于Promise更加易于理解和阅读,特别是对于有较多异步操作的代码块。 需要注意的是,async/await是基于Promise的,实际上async函数会返回一个Promise对象。在使用async/await时,可以将任何返回Promise对象的异步操作转化为同步的写法,但在某些情况下可能会导致阻塞。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值