资讯专栏INFORMATION COLUMN

以图表和示例的角度解读async/await

sutaking / 2282人阅读

摘要:在中,表示抽象的非阻塞异步执行。在完成之后安排代码的唯一方式是通过方法绑定回调函数。下图描述了该示例的计算过程方法中绑定的回调函数只有当成功的时候才会调用。为了处理失败的,需要通过绑定另一个回调函数。

介绍

ES7中,async/await 语法使异步promise的协调变得很简单。如果你需要以特定顺序异步获取来自多个数据库或API的数据,可以使用杂乱的promise或回调函数。async/await使我们可以更简便地处理这种逻辑,代码的可读性和可维护性也更好。

在该教程中,我们用图表和一些简单的例子来解释async/await的语法和语义。
开始讲解之前,我们先对promise进行一个简单的概述,如果你对promise已经很熟悉了,可以跳过该部分内容。

Promises

在js中,promise表示抽象的非阻塞异步执行。js中的promise与Java中的 Future或C#中的Task很相似。

promise通常用于网络和I/O操作-例如,读取文件,发起HTTP请求。为了不阻塞当前执行线程,我们创建一个异步promise,使用then方法绑定一个回调函数,该回调函数会在promise完成后触发。回调函数本身也可以返回一个promise,所以promise可以高效的链式调用。

简单起见,所有的例子中我们都假定request-promise库已经安装和加载完成了,如下所示:

var rp = require("request-promise");

现在我们可以像这样发起一个简单的HTTP GET请求,该方法返回一个promise:

const promise = rp("http://example.com/")

接下来,看一个例子:

console.log("Starting Execution");

const promise = rp("http://example.com/");
promise.then(result => console.log(result));

console.log("Can"t know if promise has finished yet...");

在第三行,我们创建了一个promise,然后我们在第四行中为其绑定了一个回调函数。由于promise是异步执行的
,所以执行到第六行时,我们不确定promise有没有完成。多次运行上面的代码,得到的结果可能每次都不一样。更通俗地讲,promise后面的代码和promise是并行运行的。

在promise完成之前,没有办法中断当前的操作序列。这与Java中的 Future.get是不同的,Future.get允许我们中断当前的线程直到Future完成。js中,我们不会轻易地等待promise执行完成。在promise完成之后安排代码的唯一方式是通过then方法绑定回调函数。

下图描述了该示例的计算过程:

then方法中绑定的回调函数只有当promise成功的时候才会调用。如果promise失败的话(例如,由于网络错误),回调不会执行。为了处理失败的promise,需要通过catch绑定另一个回调函数。

rp("http://example.com/").
    then(() => console.log("Success")).
    catch(e => console.log(`Failed: ${e}`))

最后,为了测试一下效果,我们通过Promise.resolvePromise.reject简单地生成成功和失败的promise:

const success = Promise.resolve("Resolved");
// Will print "Successful result: Resolved"
success.
    then(result => console.log(`Successful result: ${result}`)).
    catch(e => console.log(`Failed with: ${e}`))


const fail = Promise.reject("Err");
// Will print "Failed with: Err"
fail.
    then(result => console.log(`Successful result: ${result}`)).
    catch(e => console.log(`Failed with: ${e}`))

有关promise更详细的教程,查看这篇文章

问题-组合Promise

单个promise是很简单的。可是,我们编写复杂的异步逻辑时,可能需要组合使用多个promise来处理。大量的then语句和匿名回调函数很容易让代码变得不可维护。

例如,我们要编写一个如下功能的代码:

发起一个HTTP请求,等待完成后,打印出结果

然后发起两个并行的HTTP请求;

后两个请求都完成后,打印出他们的结果。

下面的代码片段演示了上述功能的实现:

// Make the first call
const call1Promise = rp("http://example.com/");

call1Promise.then(result1 => {
    // Executes after the first request has finished
    console.log(result1);

    const call2Promise = rp("http://example.com/");
    const call3Promise = rp("http://example.com/");

    return Promise.all([call2Promise, call3Promise]);
}).then(arr => {
    // Executes after both promises have finished
    console.log(arr[0]);
    console.log(arr[1]);
})

首先发起第一个HTTP请求,当该请求完成后,调用它的回调函数(1-3行)。在回调函数中,我们又相继发起两个HTTP请求生成了两个promise。这两个promise并行运行;当他们都执行完后,我们还需要为其绑定一个回调函数。因此,我们用promise.all将这两个promise组合成一个promise, 只有当他们都完成后,这个promise才会完成。由于第一个回调函数的结果是promise,因此我们链式地调用另一个then方法和回调函数输出最终结果。

下图描述了这个执行过程:

对于这么简单的例子,我们就用了两个then回调和promise.all来同步并行的promise。试想如果我们执行更多的异步操作或者增加错误处理函数呢?这种方式很容易让代码变成一堆杂乱的thenpromise.all和回调函数。

Async 函数

async 函数提供了一种简洁的方式来定义一个返回promise的函数。
例如,下面两种定义是等价的:

function f() {
    return Promise.resolve("TEST");
}

// asyncF is equivalent to f!
async function asyncF() {
    return "TEST";
}

相似地,在异步函数抛出异常与返回一个reject promise对象的函数等价:

function f() {
    return Promise.reject("Error");
}

// asyncF is equivalent to f!
async function asyncF() {
    throw "Error";
}
Await

我们不能同步等待promise的完成。只能通过then方法传入一个回调函数。我们鼓励非阻塞编程,因此同步等待promise是不允许的。否则,开发者会产生编写同步脚本的想法,毕竟同步编程要简单的多。

但是,为了同步promise我们需要允许他们等待彼此的完成。换句话说,如果操作是异步的(也就是说包裹在promise中),它应该可以等待其他异步操作的完成。但是,js解析器怎么知道操作是否跑在promise中?

答案是async关键字。每个async函数返回一个promise。因此,js解析器知道所有的操作都位于async函数中,并将所有的代码包裹在promise中异步地执行。所以,async函数,允许操作等待其他promise的完成。

说一下await关键字。它只能用在async函数中,允许我们同步等待promise的完成。如果在async函数外边使用promise,我们仍然需要使用then回调函数。

async function f(){
    // response will evaluate as the resolved value of the promise
    const response = await rp("http://example.com/");
    console.log(response);
}

// We can"t use await outside of async function.
// We need to use then callbacks ....
f().then(() => console.log("Finished"));

现在我们看一下前面的那个例子如何用async/await进行改写:

/ Encapsulate the solution in an async function
async function solution() {
    // Wait for the first HTTP call and print the result
    console.log(await rp("http://example.com/"));

    // Spawn the HTTP calls without waiting for them - run them concurrently
    const call2Promise = rp("http://example.com/");  // Does not wait!
    const call3Promise = rp("http://example.com/");  // Does not wait!

    // After they are both spawn - wait for both of them
    const response2 = await call2Promise;
    const response3 = await call3Promise;

    console.log(response2);
    console.log(response3);
}

// Call the async function
solution().then(() => console.log("Finished"));

以上代码,我们的解决方案就封装在了async函数中。我们可以直接await promise的执行,省掉了then回调函数。最后,我们只需要调用async函数。它封装了调用其他promise的逻辑,并返回一个promise。

实际上在上面的例子中,promise是并行触发的。本例中也一样(7-8行)。注意第12-13行我们使用了await阻塞主线程,等待所有的promise执行完成。后面,我们看到promise都完成了,和前面的例子类似(promise.all(...).then(...))。

其执行流程与前例的流程是相等的。但是,代码变得更具可读性和简洁。

底层实现上,await/async实际上转换成了promise,换句话说,await/async是promise的语法糖。每次我们使用await时,js解析器会生成一个promise,并将async函数中的剩余代码放到then回调中去执行。
思考下面的例子:

async function f() {
    console.log("Starting F");
    const result = await rp("http://example.com/");
    console.log(result);
}

下面描述函数f的基本计算过程。由于f是异步的,它会与调用方并行执行:


函数f开始执行,遇到await后生成一个promise。此时,函数的其余部分被封装在回调中,并在promise完成后执行。

错误处理

前面的大部分例子中,我们都是假设promise成功完成了。因此,等待promise返回一个值。如果我们等待的promise失败了,在async函数中会导致一个异常。我们可以使用标准的try/catch来捕获和处理它。

async function f() {
    try {
        const promiseResult = await Promise.reject("Error");
    } catch (e){
        console.log(e);
    }
}

如果async函数没有处理异常,不管是promise reject了,还是产生了其他bug,它都会返回一个rejected的promise对象。

async function f() {
    // Throws an exception
    const promiseResult = await Promise.reject("Error");
}

// Will print "Error"
f().
    then(() => console.log("Success")).
    catch(err => console.log(err))

async function g() {
    throw "Error";
}

// Will print "Error"
g().
    then(() => console.log("Success")).
    catch(err => console.log(err))

这给我们提供了一种简便的方法,通过已知的异常处理机制来处理被rejected的promise。

讨论

async/await 在语言结构上是对promise的补充。但是,async/await 并不能取代纯promise的需求。例如,在正常函数和全局作用域我们不能使用await,所以需要使用普通的promise:

async function fAsync() {
    // actual return value is Promise.resolve(5)
    return 5;
}

// can"t call "await fAsync()". Need to use then/catch
fAsync().then(r => console.log(`result is ${r}`));

我通常会将异步逻辑封装到一个或者少数几个async函数中,然后在非异步代码中调用async函数。这样我可以最小化降低书写then/catch的数量。
学者们指出,并发性和并行性是有区别的。并发性是指将独立的进程(一般意义上的进程)组合在一起,而并行实际上是同时执行多个进程。并发性是关于应用程序设计和结构的,而并行性是关于实际执行的。

我们以一个多线程应用程序为例。应用程序分离到线程定义了它的并发模型。这些线程在可用内核上的映射定义了它的级别或并行性。并发系统可以在单个处理器上高效运行,在这种情况下,它不是并行的。

就此而言,promise允许我们将一个程序分解为并行的并发模块,也可以不并行运行。实际的JavaScript执行是否并行取决于实现。例如,Node Js是单线程的,如果一个promise是CPU绑定的,你就不会看到太多的并行性。然而,如果你通过像Nashorn这样的东西把你的代码编译成java字节码,理论上你可能能够在不同核心上映射CPU绑定的promise,并且实现并行性。因此,在我看来,promise(无论是普通的或通过await/async)构成了JavaScript应用程序的并发模型。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89634.html

相关文章

  • 可靠React组件设计7个准则之SRP

    摘要:编写组件时要考虑的基本准则是单一职责原则。这些更改通常要求组件在隔离状态下易于修改这也是的目标。解决多重责任问题需要将分割为两个组件和。组件之间的通信是通过实现。更改的唯一原因是修改表单字段。 翻译:刘小夕原文链接:https://dmitripavlutin.com/7-... 原文的篇幅非常长,不过内容太过于吸引我,还是忍不住要翻译出来。此篇文章对编写可重用和可维护的React组...

    Charles 评论0 收藏0
  • 【译】async/await 应知应会

    摘要:原文地址原文作者翻译作者是在版本中引入的,它对于中的异步编程而言是一个巨大的提升。可能会产生误导一些文章把和进行了比较,同时说它是异步编程演变过程中的下一代解决方案,对此我不敢苟同。结论在中引入的关键字无疑是对异步编程的一大加强。 原文地址: https://hackernoon.com/javasc...原文作者: Charlee Li 翻译作者: Xixi20160512 asy...

    Ku_Andrew 评论0 收藏0
  • 黄金搭档 -- JS 装饰器(Decorator)与Node.js路由

    摘要:即为装饰器函数的这里主要为了获取路由路径的前缀,为请求方法,为请求路径,为请求执行的函数。下边是设置路由路径前缀和塞入内容的装饰器函数就不多说了,就是挂载前缀路径到类的原型对象上,这里需要注意的是作用于类,所以是被修饰的类本身。 很多面对象语言中都有装饰器(Decorator)函数的概念,Javascript语言的ES7标准中也提及了Decorator,个人认为装饰器是和async/a...

    simon_chen 评论0 收藏0
  • 如何正确合理使用 JavaScript async/await

    摘要:想阅读更多优质文章请猛戳博客一年百来篇优质文章等着你引入的在的异步编程中是一个极好的改进。可能会产生误导一些文章将与进行了比较,并声称它是下一代异步编程风格,对此作者深表异议。结论引入的关键字无疑是对异步编程的改进。 showImg(https://segmentfault.com/img/bVbjFP0?w=800&h=450); 想阅读更多优质文章请猛戳GitHub博客,一年百来篇...

    trigkit4 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<