JavaScript 异步函数

本文详细探讨了异步函数、async/await的关键字及其用法,涉及执行顺序、睡眠函数、并行与串行执行策略,以及异步函数与期约在内存管理上的区别。同时,文章还解释了栈追踪在异步和期约场景下的差异。
摘要由CSDN通过智能技术生成

目录

1、异步函数

1.1 async

1.2 await

2、停止和恢复执行

2.1 async/await 执行顺序1

2.2 async/await 执行顺序2

3. 异步函数策略

3.1 使用 sleep()

3.2 利用平行执行

3.3 串行执行期约

3.4 栈追踪与内存管理


1、异步函数

异步函数,也称为“async/await”(语法关键字),是ES6期约模式在ECMAScript函数中的应用。

async/await是ES8规范新增的,这个特性从行为和语法上都增强了JavaScript,让以同步方式写的代码能够异步执行。

1.1 async

async关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上:

  async function foo() {}
  let bar = async function () {}
  let baz = async () => {}
  class O {
    async f() {}
  }

使用async关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。

foo()函数仍然会在后面的指令之前被求值:

async function foo() {
  console.log(1);
}
foo();
console.log(2);
// 1
// 2

异步函数如果使用return关键字返回了值(默认为undefined),这个值会被Promise.resolve()包装成一个期约对象。

async function foo() {
  console.log(1);
  return Promise.resolve(3);
}
// 给返回的期约添加一个解决处理程序
foo().then((x)=> {
  console.log(x);
});
console.log(2);
// 1
// 2
// 3

异步函数的返回值期待(但实际上并不要求)一个实现thenable接口的对象,但常规的值也可以。

如果返回的是实现thenable接口的对象,则这个对象可以由提供给then()的处理程序“解包”。

如果不是,则返回值就被当作已经解决的期约。

// 返回一个原始值
async function foo() {
  return 'foo';
}
foo().then(console.log);
// foo
// 返回一个没有实现thenable接口的对象
async function bar() {
  return ['bar'];
}
bar().then(console.log);
// ['bar']
// 返回一个实现了thenable接口的非期约对象
async function baz() {
  constthenable={
    then(callback){callback('baz');}
  };
  return thenable;
}
baz().then(console.log);
// baz
// 返回一个期约
async function qux() {
  return Promise.resolve('qux');
}
qux().then(console.log);
// qux

与在期约处理程序中一样,在异步函数中抛出错误会返回拒绝的期约:

async function foo() {
  console.log(1);
  throw3;
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3

拒绝期约的错误不会被异步函数捕获:

async function foo() {
  console.log(1);
  Promise.reject(3);
}
// Attach a rejected handler to the returned promise
foo().catch(console.log);
console.log(2);
// 1
// 2
//Uncaught(inpromise): 3

1.2 await

因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。

使用await关键字可以暂停异步函数代码的执行,等待期约解决。

使用期约的写法

let p = new Promise((resove, reject) => setTimeout(resove, 1000, 1));
p.then((x) => console.log(x)); // 1

使用async/await改造

async function foo() {
  let p = new Promise((resove, reject) => setTimeout(resove, 1000, 1));
  console.log(await p);
}

foo(); 
// 1

注意,await关键字会暂停执行异步函数后面的代码,让出JavaScript运行时的执行线程。(后面会详细介绍)

  async function foo() {
    console.log(1);
    let p = new Promise((resove, reject) => setTimeout(resove, 1000, 2));
    console.log(await p);
    console.log(3); 
  }
  async function bar () {
    console.log(4)
  }

  foo();
  bar();
  // 1
  // 4
  // 2
  // 3

 await关键字期待(但实际上并不要求)一个实现thenable接口的对象,但常规的值也可以。

如果是实现thenable接口的对象,则这个对象可以由await来“解包”。

如果不是,则这个值就被当作已经解决的期约。下面的代码演示了这些情况:

// 等待一个原始值
async function foo() {
  console.log(await 'foo');
}
foo();
// foo
// 等待一个没有实现thenable接口的对象
async function bar() {
  console.log(await ['bar']);
}
bar();
// ['bar']
// 等待一个实现了thenable接口的非期约对象
async function baz() {
  const thenable ={
    then(callback) { callback('baz'); }
  };
  console.log(await thenable);
}
baz();
// baz
// 等待一个期约
async function qux() {
  console.log(await Promise.resolve('qux'));
}
qux();
// qux

等待会抛出错误的同步操作,会返回拒绝的期约:

async function foo() {
  console.log(1);
  await(() => { throw 3; })();
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3

如前面的例子所示,单独的Promise.reject()不会被异步函数捕获,而会抛出未捕获错误。

不过,对拒绝的期约使用await则会释放(unwrap)错误值(将拒绝期约返回):

async function foo() {
  console.log(1);
  await Promise.reject(3);
  console.log(4); // 这行代码不会执行
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3

await 的限制

await关键字必须在异步函数中使用

异步函数的特质不会扩展到嵌套函数

2、停止和恢复执行

使用await关键字之后的区别其实比看上去的还要微妙一些。

比如,下面的例子中按顺序调用了3个函数,但它们的输出结果顺序是相反的:

但是我执行后的输出顺序是‘baz’-'foo'-'bar'

async function foo() {
  console.log(await Promise.resolve('foo'));
}
async function bar() {
  console.log(await 'bar');
}
async function baz() {
  console.log('baz');
}
foo();
bar();
baz();
// baz
// bar
// foo

2.1 async/await 执行顺序1

要完全理解await关键字,必须知道它并非只是等待一个值可用那么简单。

JavaScript运行时在碰到await关键字时,会记录在哪里暂停执行。

等到await右边的值可用了,JavaScript运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。

因此,即使await后面跟着一个立即可用的值,也会被异步求值。如下:

async function foo() {
  console.log(2);
  await null;
  console.log(4);
}
console.log(1);
foo();
console.log(3);
// 1
// 2
// 3
// 4

代码执行步骤

(1)打印1;

(2)调用异步函数foo();

(3)(在foo()中)打印2;

(4)(在foo()中)await关键字暂停执行,为立即可用的值null向消息队列中添加一个任务;

(5)foo()退出;

(6)打印3;

(7)同步线程的代码执行完毕;

(8)JavaScript运行时从消息队列中取出任务,恢复异步函数执行;

(9)(在foo()中)恢复执行,await取得null值(这里并没有使用);

(10)(在foo()中)打印4;

(11)foo()返回。

2.2 async/await 执行顺序2

如果await后面是一个期约,则问题会稍微复杂一些。

此时,为了执行异步函数,实际上会有两个任务被添加到消息队列并被异步求值。

下面的例子虽然看起来很反直觉,但它演示了真正的执行顺序:

我在编辑器中执行代码输出顺序是先8、9后6 、7

async function foo() {
    console.log(2);
    console.log(await Promise.resolve(8));
    console.log(9);
  }
  async function bar() {
    console.log(4);
    console.log(await 6);
    console.log(7);
  }
  console.log(1);
  foo();
  console.log(3);
  bar();
  console.log(5);
  // 1
  // 2
  // 3
  // 4
  // 5
  // 6
  // 7
  // 8
  // 9

代码执行步骤

(1)打印1;

(2)调用异步函数foo();

(3)(在foo()中)打印2;

(4)(在foo()中)await关键字暂停执行,向消息队列中添加一个期约在落定之后执行的任务;

(5)期约立即落定,把给await提供值的任务添加到消息队列;

(6)foo()退出;

(7)打印3;

(8)调用异步函数bar();

(9)(在bar()中)打印4;

(10)(在bar()中)await关键字暂停执行,为立即可用的值6向消息队列中添加一个任务;

(11)bar()退出;

(12)打印5;

(13)顶级线程执行完毕;

(14)JavaScript运行时从消息队列中取出解决await期约的处理程序,并将解决的值8提供给它;

(15)JavaScript运行时向消息队列中添加一个恢复执行foo()函数的任务;

(16)JavaScript运行时从消息队列中取出恢复执行bar()的任务及值6;

(17)(在bar()中)恢复执行,await取得值6;

(18)(在bar()中)打印6;

(19)(在bar()中)打印7;

(20)bar()返回;

(21)异步任务完成,JavaScript从消息队列中取出恢复执行foo()的任务及值8;

(22)(在foo()中)打印8;

(23)(在foo()中)打印9;

(24)foo()返回。

3. 异步函数策略

在使用异步函数时,还是有些问题要注意。

3.1 使用 sleep()

  async function sleep (dleay) {
    return new Promise((resolve, reject)=> setTimeout(resolve, dleay));
  }
  async function foo() {
    const t0 = Date.now();
    await sleep(1000); // 暂停约 1000 毫秒
    console.log(Date.now() - t0);
  }
  foo();
  // 1015

3.2 利用平行执行

如果使用await时不留心,则很可能错过平行加速的机会。

来看下面的例子,其中顺序等待了5个随机的超时:

async function randomDelay(id) {
  // 延迟0~1000 毫秒
  const delay = Math.random() * 1000;
  return new Promise((resolve) => setTimeout(() => {
    console.log(`${id} finished`);
    resolve();
  }, delay));
}
async function foo() {
  const t0 = Date.now();
  await randomDelay(0);
  await randomDelay(1);
  await randomDelay(2);
  await randomDelay(3);
  await randomDelay(4);
  console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 0 finished
// 1 finished
// 2 finished
// 3 finished
// 4 finished
// 877ms elapsed

用一个for循环重写,就是:

async function randomDelay(id) {
    // 延迟0~1000 毫秒
    const delay = Math.random() * 1000;
    return new Promise((resolve) => setTimeout(() => {
      console.log(`${id} finished`);
      resolve();
    }, delay));
  }
  async function foo() {
    const t0 = Date.now();
    for(let i=0;i<5;++i){
      await randomDelay(i);
    }
    console.log(`${Date.now() - t0}ms elapsed`);
  }
  foo();
  // 0 finished
  // 1 finished
  // 2 finished
  // 3 finished
  // 4 finished
  // 877ms elapsed

用数组和for循环再包装一下就是:

async function randomDelay(id) {
  // 延迟0~1000 毫秒
  const delay = Math.random() * 1000;
  return new Promise((resolve) =>
    setTimeout(() => {
      console.log(`${id} finished`);
      resolve();
    }, delay)
  );
}
async function foo() {
  const t0 = Date.now();
  const promises = Array(5)
    .fill(null)
    .map((_, i) => randomDelay(i));
  for (const p of promises) {
    await p;
  }
  console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 4 finished
// 2 finished
// 1 finished
// 0 finished
// 3 finished
// 877ms elapsed

注意,虽然期约没有按照顺序执行,但await按顺序收到了每个期约的值:

async function randomDelay(id) {
  // 延迟0~1000 毫秒
  const delay = Math.random() * 1000;
  return new Promise((resolve) => setTimeout(() => {
    console.log(`${id} finished`);
    resolve(id);
  }, delay));
}
async function foo() {
  const t0 = Date.now();
  const promises = Array(5).fill(null).map((_, i) => randomDelay(i));
  for (const p of promises) {
    console.log(`awaited ${await p}`);
  }
  console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 1 finished
// 2 finished
// 4 finished
// 3 finished
// 0 finished
// awaited 0
// awaited 1
// awaited 2
// awaited 3
// awaited 4
// 645ms elapsed

3.3 串行执行期约

使用async/await,期约连锁会变得很简单:

async function addTwo(x) {
  return x + 2;
}
async function addThree(x) {
  return x + 3;
}
async function addFive(x) {
  return x + 5;
}
async function addTen(x) {
  for (const fn of [addTwo, addThree, addFive]) {
    x = await fn(x);
  }
  return x;
}
addTen(9).then(console.log); // 19

3.4 栈追踪与内存管理

期约与异步函数的功能有相当程度的重叠,但它们在内存中的表示则差别很大。

看看下面的例子,它展示了拒绝期约的栈追踪信息:

function fooPromiseExecutor(resolve, reject) {
  setTimeout(reject, 1000, 'bar');
}
function foo() {
  new Promise(fooPromiseExecutor);
}
foo();
// Uncaught (in promise) bar
//    setTimeout
//    setTimeout (async)
//   fooPromiseExecutor
//   foo

根据对期约的不同理解程度,以上栈追踪信息可能会让某些读者不解。

栈追踪信息应该相当直接地表现JavaScript引擎当前栈内存中函数调用之间的嵌套关系。

在超时处理程序执行时和拒绝期约时,我们看到的错误信息包含嵌套函数的标识符,那是被调用以创建最初期约实例的函数。

可是,我们知道这些函数已经返回了,因此栈追踪信息中不应该看到它们。

答案很简单,这是因为JavaScript引擎会在创建期约时尽可能保留完整的调用栈。

在抛出错误时,调用栈可以由运行时的错误处理逻辑获取,因而就会出现在栈追踪信息中。

当然,这意味着栈追踪信息会占用内存,从而带来一些计算和存储成本。

如果在前面的例子中使用的是异步函数,那又会怎样呢?比如:

function fooPromiseExecutor(resolve, reject) {
  setTimeout(reject, 1000, 'bar');
}
async function foo() {
  await new Promise(fooPromiseExecutor);
}
foo();
// Uncaught (in promise) bar
//   foo
//   asyncfunction(async)
//   foo

这样一改,栈追踪信息就准确地反映了当前的调用栈。

fooPromiseExecutor()已经返回,所以它不在错误信息中。

但foo()此时被挂起了,并没有退出。

JavaScript运行时可以简单地在嵌套函数中存储指向包含函数的指针,就跟对待同步函数调用栈一样。

这个指针实际上存储在内存中,可用于在出错时生成栈追踪信息。

这样就不会像之前的例子那样带来额外的消耗,因此在重视性能的应用中是可以优先考虑的。

  • 19
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CJ-杰

打赏描述

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值