1.3.3 Promise与async/await

CompletableFuture还实现了另一个接口——CompletionStage,前面我们用到的thenAccept类似的方法也都是这个接口的API。从定义和功能来看,CompletionStage是一个Promise。

那么Promise又是什么呢?按照Promises/A+(https://promisesaplus.com/)给出的定义,Promise是一个异步任务,它存在挂起完成拒绝三个状态,当它处在完成状态时,结果通过调用then方法的参数进行回调;出现异常拒绝时,通过catch方法传入的参数来捕获拒绝的原因。

从ECMAScript 6开始,JavaScript就已经支持Promise了,我们先来看之前的例子怎么用Promise来实现,如代码清单1-19所示。

代码清单1-19 使用Promise批量获取图片(JavaScript)


function bitmapPromise(url) {
  return new Promise((resolve, reject) => {
    try {
      download(url, resolve)
    } catch (e) {
      reject(e)
    }
  })
}

const urls = ...; // 省略url的获取
Promise.all(urls.map(url => bitmapPromise(url)))
  .then(bitmaps => console.log(bitmaps))
  .catch(e => console.error(e))

我们注意到,JavaScript的Lambda语法更接近Java 8。以url=>bitmapPromise(url)为例,url是参数,bitmapPromise(url)是表达式体,由于只有一行,因此它的返回值也是Lambda表达式的返回值。

我们通过bitmapPromise函数创建Promise实例,后者接收一个Lambda表达式,这个Lambda表达式有两个参数,resolve和reject,分别对应完成拒绝状态的回调。其中resolve会作为参数传给download函数,在图片获取完成之后回调。

Promise.all会将多个Promise整合到一起,这与我们前面为整合CompletableFuture而定义的List<CompletableFuture<T>>.allOf如出一辙。最终我们得到一个新的Promise,它的结果是整合了前面所有bitmapPromise函数返回的结果的bitmaps,因此我们在then当中传入的Lambda表达式就是用来处理消费这个bitmaps的。

这样看起来很不错,达到了与CompletableFuture同样的效果,不过还可以更简洁。我们可以通过async/await将上面的代码进一步简化,如代码清单1-20所示。

代码清单1-20 使用async/await


async function main() {
  try {
    const bitmaps = await Promise.all(urls.map(url => bitmapPromise(url)));
    console.log(bitmaps);
  } catch (e) {
    console.error(e);
  }
}

我们给整个逻辑的外部函数加上了async关键字,这样就可以在异步调用返回Promise的位置加上await,这个语法糖可以把前面的then和catch调用转换成我们熟悉的同步调用语法。这样看上去逻辑是否清楚多了呢?

当然,由于每个bitmapPromise函数返回的都是Promise,因此我们也可以对每一个Promise进行await,如代码清单1-21所示。

代码清单1-21 循环中使用async/await


async function main() {
  try {
    const promises = urls.map(url => bitmapPromise(url));
    const bitmaps = [];
    for (const promise of promises) {
      bitmaps.push(await promise)
    }
    console.log(bitmaps);
  } catch (e) {
    console.error(e);
  }
}

async和await很好地兼顾了异步任务执行和同步语法结构的需求,凡是有过回调开发经验的开发者都可以很容易理解它的内在含义。以下几个较为流行的语言也支持这一特性:

·JavaScript ES 2016(ES7)

·C#5.0

·Python 3.5

·Rust 1.39.0

而本书的主角Kotlin对async/await的支持稍微有些不同,它没有引入这两个关键字就实现了这一功能。具体如何做到这一点,我们将在后面详细介绍。

注意 本书在介绍协程概念时,会涉及与其他语言的对比,例如常见的JavaScript、Go等。这些语言都偏向命令式风格,语法上与Java、Kotlin相近,通过我们的剖析,读者应该可以大致理解它们的执行过程。这些内容仅做了解即可。