zoukankan      html  css  js  c++  java
  • 图与例解读Async/Await

    JavaScript ES7的async/await语法让异步promise操作起来更方便。如果你需要从多个数据库或者接口按顺序异步获取数据,你可能最终写出一坨纠缠不清的promise与回调。然而使用async/await可以让我们用更加可读、可维护的方式来表达这种逻辑。

    这篇教程以图表与简单例子来阐述JS async/await的语法与运行机理。

    在深入之前,我们先简单回顾一下promise,如果对这方面概念有自信,大可自行跳过。

    Promise

    在JS的世界里,一个promise抽象表达一个非阻塞(阻塞指一个任务开始后,要等待该任务执行结果产生之后才继续执行后续任务)的异步流程,类似于Java的Futrue或者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...");
    

    我们在第3行产生了一个promise,然后在第4行附上了一个回调函数。返回的promise是异步的,所以当执行的第6行的时候,我们无法确定这个promise有没有完成,多次执行可能有不同的结果(译者:浏览器里执行多少次,这里promise都会是未完成状态)。概括来说,promise之后的代码跟promise自身是并发的(译者:对这句话有异议者参见本文最后一节的并发说明)。

    并不存在一种方法可以让当前的执行流程阻塞直到promise完成,这一点与Java的Futrue.get相异。JS里,我们无法直接原地等promise完成,唯一可以用于提前计划promise完成后的执行逻辑的方式就是通过then附加回调函数。

    下面的图表描绘了上面代码例子的执行过程:
    这里写图片描述

    Promise的执行过程,调用“线程”无法直接等待promise结果。唯一规划promise之后逻辑的方法是使用then方法附加一个回调函数。

    通过then 附加的回调函数只会在promise成功是被触发,如果失败了(比如网络异常),这个回调不会执行,处理错误需要通过catch 方法:

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

    最后,为了方便试验功能,我们可以直接创建一些“假想”的promise,使用Promise.resolve生成会直接成功或失败的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组合起来。写许多的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行)。在回调里,我们为另外两次请求制造了promise(第8-9行)。这两个promise并发运行,我们需要计划一个在两个都完成时执行的回调,于是,我们通过Promise.all(第11行)来讲他们合并。这第一个回调的返回值是一个promise,我们再添加一个then来输出结果(第12-16行)。

    以下图标描绘这个计算过程:
    这里写图片描述

    将promise组合的计算过程。使用“Promise.all”将两个并发的promise合并成一个。

    为了一个简单的例子,我们最终写了两个then回调以及一个Promise.all来同步两个并发promise。如果我们还想再多做几个异步操作或者添加一些错误处理会怎样?这种实现方案最终很容变为纠缠成一坨的then、Promise.all以及回调匿名函数。

    Async函数

    一个async函数是定义会返回promise的函数的简便写法。

    比如,以下两个定义是等效的:

    function f() {
        return Promise.resolve('TEST');
    }
    
    // asyncF is equivalent to f!
    async function asyncF() {
        return 'TEST';
    }
    

    相似地,会抛出错误的async函数等效于返回将失败的promise 的函数:

    function f() {
        return Promise.reject('Error');
    }
    
    // asyncF is equivalent to f!
    async function asyncF() {
        throw 'Error';
    }
    

    Await

    以前,当我们产生一个promise,我们无法同步地等待它完成,我们只能通过then注册一个回调函数。不允许直接等待一个promise是为了鼓励开发者写非阻塞的代码,不然开发者会更乐意写阻塞的代码,因为这样比promise和回调简单。

    然而,为了同步多个promise,我们需要它们互相等待,换句话说,如果一个操作本身就是异步的(比如,用promise包装的),它应该具备能力等待另一个异步操作先完成。但是JS解释器如何知道一个操作是不是在一个promise里的?

    答案就是async关键字,所有的async函数一定会返回一个promise。所以,JS解释器也可以确信async函数里操作是用promise包装的异步过程。于是也就可以允许它等待其他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'));
    

    现在我们来看看我们可以如何解决之前提到的问题:

    // 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函数里。这样我们就可以直接对promise使用await了,也就规避了写then回调。最后我们调用这个async函数,然后按照普通的方式使用返回的promise。

    要注意的是,在第一个例子里(没有async/await),后面两个promise是并发的。所以我们在第7-8行也是如此,然后直到11-12行才用await来等待两个promise都完成。这之后,我们可以确信两个promise都已经完成(与之前Promise.all(...).then(...)类似)。

    计算流程跟之前的图表描绘的一样,但是代码变得更加已读与直白。

    事实上,async/await其实会翻译成promise与then回调(译者:babel其实是翻译成generator语法,再通过类似co的函数运行,co内部运行机制离不开promise)。每次我们使用await,解释器会创建一个promise然后把async函数的后续代码放到then回调里。

    我们来看看以下的例子:

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

    f函数的内在运行过程如下图所描绘。因为f标记了async,它会与它的调用者“并发”:
    这里写图片描述
    函数f启动并产生一个promise。在这一刻,函数剩下的部分都会被封装到一个回调函数里,并被计划在promise完成之后执行。

    错误处理

    在之前的例子里,我们大多假定promise会成功,然后await一个promise的返回值。如果我们等待的promise失败了,会在async函数里产生一个异常,我们可以使用标准的try/catch来处理它

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

    如果async函数不处理这个异常,不管是这异常是因为promise是被reject了还是其他的bug,这个函数都会返回一个被reject掉的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))
    

    这就让我们可以使用熟悉的方式来处理错误。

    扩展说明

    async/await是一个对promise进行补充的语法部件,它能让我们写更少的重复代码来使用promise。然而,async/await并不能彻底取代普通的promise。比如,如果我们在一个普通的函数或者全局作用域里使用一个async函数,我们无法使用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回调。

    async / await是让promise用起来更简洁的语法糖。所有的async / await都可以用普通的promise来实现。所有总结来说,这只是个代码样式与简洁的问题。

    学院派的人会指出,并发与并行是有区别的(译者:所以前文都是说并发,而非并行)。参见Rob Pike的讲话或者我之前的博文。并发是组合多个独立过程来一起工作,并行是多个过程同时执行。并发是体现在应用的结构设计,并行是实际执行的方式。

    我们来看看一个多线程应用的例子。将应用分割成多个线程是该应用并发模型的定义,将这些线程放到可用的cpu核心上执行是确立它的并行。一个并发的系统也可以在一个单核处理器上正常运行,但这种情况并不是并行。
    这里写图片描述
    以这种方式理解,promise可以将一个程序分解成多个并发的模块,它们或许,也可能并不会并行执行。JS是否并行执行要看解释器自身的实现。比如,NodeJS是单线程的,如果一个promise里有大量的CPU操作(非I/O操作),你可能感受不到太多并行。然而如果你用像nashorn这样的工具把代码编译成java字节码,理论上你可以把繁重的CPU操作放到其他内核上来获得平行效果。于是在我的观点中,promise(不管是裸的还是有async/await)只是作用于定义JS应用的并发模型(而非确定逻辑是否会并行运行)。

    关于本文

    译者:@安秦

    译文:https://zhuanlan.zihu.com/p/30500864

    作者:@Nikolay

    原文:http://nikgoozev.com/2017/10/01/async-await/

  • 相关阅读:
    运营商公网
    任务管理器 的 服务与进程
    QQ通信原理及QQ是怎么穿透内网进行通信的?
    windows Telnet 客户端常用命令介绍
    redis优化
    shell反射
    USB安装centos6系统(centos7需要换软件)
    rocketmq双主模式
    golang数据类型与转换
    golang介绍
  • 原文地址:https://www.cnblogs.com/mr-yuan/p/7752946.html
Copyright © 2011-2022 走看看