async 函数及串行与并行

307 阅读5分钟

1. 是什么?

async function 声明创建一个绑定到给定名称的新异步函数。函数体内允许使用 await 关键字,这使得我们可以更简洁地编写基于 promise 的异步代码,并且避免了显式地配置 promise 链的需要。

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

// async 写法
const asyncReadFile = async function () {
  const f1 = await readFile('/etc/abc');
  const f2 = await readFile('/etc/def');
};

1.1. 和 Generator 函数 对比

  • async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await
  • Generator 函数的执行必须靠执行器,而async函数自带执行器。即 async函数的执行,与普通函数一样。
// Generator 写法
const gen = function* () {
  const f1 = yield readFile('/etc/abc');
  const f2 = yield readFile('/etc/def');
};

1.2. 和 Promise 调用 对比

// Promise 写法
const promiseReadFile = function () {
  let p1 = new Promise((resolve)=>{
    readFile('/etc/abc');
    resolve(true)
  }).then(v=>console.log(v))
  let p2 = new Promise((resolve)=>{
    readFile('/etc/def');
  })
};

2. 基本用法

2.1. async

  • async声明的函数返回的是Promise对象,可then方法添加回调函数
  • 只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
  • async函数内部return语句返回,会成为then方法回调函数的参数,若没有return的值,则参数为undefined

2.2. await

  • await必须在async声明的函数内使用。
  • await命令后面必须是 Promise 对象,然后返回该对象的结果。
    • await后面是 Promise 对象本身;
    • await后面是原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolvedPromise 对象)。然后直接返回对应的值。
    • await后面是一个thenable对象(即定义了then方法的对象),await会将其视为Promise处理。
  • 当函数执行的时候,一旦遇到await就会先不往下执行,等 await 后的异步操作完成,再接着执行函数体内后面的语句。
function timeout(ms) {
  console.log('timeout...', ms)
  return new Promise((resolve) => {
    console.log('timeout promise...', resolve)
    setTimeout(()=>resolve('t resolve'), ms);
  });
}

async function asyncPrint(value, ms) {
  console.log(1)
  // await命令后面的 Promise 对象,接收其resolve/reject 的参数
  let p = await timeout(ms);
  console.log(2, p, value, ms);
  return value
}

// async 函数返回一个Promise对象, eg:asy
let asy = asyncPrint('hello world', 50)
console.log('asy...', asy)

// then 接收回调结果,参数是 async 内 return 的返回值, eg:v
// 只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
asy.then(v => console.log('then...',v));

console.log(3)

// 输出:
// 1
// timeout... 50
// timeout promise... ƒ () { [native code] }
// asy... Promise{}
// 3

// 2 't return' 'hello world' 50
// then... hello world

2.3. 错误处理

如果await后面的异步操作出错,那么等同于async函数返回的 Promise对象被reject

为了防止出错,将await操作放在try...catch代码块之中。

async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);

    console.log('Final: ', val3);
  }
  catch (err) {
    console.error(err);
  }
}

3. 易错点

3.1. await命令后面必须放Promise对象 或 原始数据类型值、thenable对象。

3.2. await命令后的Promise对象声明时需要有有完整的从 pending 变为 resolved/reject 的部分,否则 Promise 会一直处于 pending 状态。

function timeoutPending() {
  return new Promise(()=>{
    setTimeout(()=>{
      console.log('timeout-Pending')
  },1)
  })
}
function timeout() {
  return new Promise((resolve)=>{
     setTimeout(()=>{
       console.log('timeout-Success')
       resolve('success')
     }, 10)
  })
}

async function getFoo() {
  // 返回 fulfilled 状态
  let foo2 = await timeout()
  console.log('2', foo2);
  
  // 返回 pending 状态,不会往下执行
  let foo1 = await timeoutPending()
  console.log('1', foo1);
  console.log(3);
}

// 输出:
// timeout-Success
// 2 success
// timeout-Pending

3.3. 多个请求串行与并行

3.3.1. 如下是串行与并行,两种写法的例子
  • 写法1:

function getFoo() {
  return new Promise((res)=>{
    setTimeout(()=>res('getFoo'),2000)
  })
}
function getBar() {
  return new Promise((res)=>{
    setTimeout(()=>res('getBar'),1000)
  })
}

// 串行
async function getAll() {
  console.log('1', new Date().getSeconds());
  let foo = await getFoo();
  console.log('2', new Date().getSeconds(), foo);
  let bar = await getBar();
  console.log('3', new Date().getSeconds(), bar);
}

getAll()
// 输出:
// 1 29
// Promise {<fulfilled>}
// 2 31 getFoo
// 3 32 getBar
function getFoo() {
  return new Promise((res)=>{
    setTimeout(()=>res('getFoo'),2000)
  })
}
function getBar() {
  return new Promise((res)=>{
    setTimeout(()=>res('getBar'),1000)
  })
}

// 并行
async function getAll() {
  console.log('1', new Date().getSeconds());
  let f = getFoo();
  let b = getBar()
  let foo = await f;
  console.log('2', new Date().getSeconds(), foo);
  let bar = await b;
  console.log('3', new Date().getSeconds(), bar);
}

getAll()
// 输出:
// 1 32
// Promise {<fulfilled>}
// 2 34 getFoo
// 3 34 getBar
  • 写法2:

// 串行
async function getAll() {
  let getFoo = new Promise((res)=>{
    setTimeout(()=>res('getFoo'),2000)
  })
  console.log('1', new Date().getSeconds());
  let foo = await getFoo;
  console.log('2', new Date().getSeconds(), foo);
  let getBar = new Promise((res)=>{
    setTimeout(()=>res('getBar'),1000)
  })
  let bar = await getBar;
  console.log('3', new Date().getSeconds(), bar);
}

getAll()
// 输出:
// 1 45
// Promise {<fulfilled>}
// 2 47 getFoo
// 3 48 getBar
// 并行
async function getAll() {
  console.log('1', new Date().getSeconds());
  let getFoo = new Promise((res)=>{
    setTimeout(()=>res('getFoo'),2000)
  })
  let getBar = new Promise((res)=>{
    setTimeout(()=>res('getBar'),1000)
  })
  let foo = await getFoo;
  console.log('2', new Date().getSeconds(), foo);
  let bar = await getBar;
  console.log('3', new Date().getSeconds(), bar);
}

getAll()
// 输出:
// 1 36
// Promise {<fulfilled>}
// 2 38 getFoo
// 3 38 getBar
3.3.2. 分析
  1. 现象

多个请求串行,是后一个等前一个执行成功后再去请求;多个请求并行,是所有请求一起去执行。

如上左侧是串行的例子,串行代码总执行时间需要3s,并行代码只需2s。

  1. 原因分析

如上例子串行与并行请求代码的区别,通过调整代码顺序即可实现。 可为什么呢?

其原因是 Promise的运行机制, Promise 新建(即: new Promise() )后就会立即执行。

如写法2:

  • 串行代码,在创建 getFoo 变量时,就开始执行了new Promise()内的代码了,遇到await后,等待pending状态改变为fulfilled/reject 后,继续执行下面的语句,过程同前面;
  • 而并行代码,一开始先创建getFoogetBar 变量,就意味着将两个new Promise()内的代码的代码都执行了,再遇到await,等到pending状态改变为fulfilled/reject 后,继续执行下一个await
    • 如果第一个需要2s,第二个需要1s,则第一个await结果返回后,第二个结果也已经返回,则如例子所示,两个的时间是一样的;
    • 如果如果第一个需要1s,第二个需要2s,则第一个await结果返回后,被第二个await拦住等待状态改变,改变后再继续往下执行。

附:串、并行的 Promise 写法:

// 并行
async function getAll() {
  console.log('1', new Date().getSeconds());
  let getFoo = new Promise((res)=>{
    setTimeout(()=>{res('getFoo');console.log('2', new Date().getSeconds(), getFoo);},2000)
  })
  let getBar = new Promise((res)=>{
    setTimeout(()=>{res('getBar');console.log('3', new Date().getSeconds(), getBar)},1000)
  })
}

// 串行
async function getAll() {
  console.log('1', new Date().getSeconds());
  let getFoo = new Promise((res)=>{
    setTimeout(()=>{res('getFoo');console.log('2', new Date().getSeconds(), getFoo);},2000)
  }).then((v)=>{
      let getBar = new Promise((res)=>{
        setTimeout(()=>{res('getBar');console.log('3', new Date().getSeconds(), getBar)},1000)
      })
  })
}

内容参考

developer.mozilla.org/zh-CN/docs/… es6.ruanyifeng.com/#docs/async