前言

        异步编程是一种处理并发操作的方法,使得程序可以在等待耗时操作(如文件读写、网络请求或数据库查询),完成的同时继续执行其他任务。与同步编程不同,异步编程不要求程序按顺序等待每个操作完成,而是通过回调、Promise或者async/await等机制,在操作完成时再执行特定的逻辑。

        通俗的讲,异步编程即是允许我们在执行一个长时间的任务时,程序不需要等待,而是继续执行之后的代码,知道这些任务完成之后再回来通知你,通常是以回调函数(callback)的方式。这种编程模式避免了程序的堵塞,大大提高了cpu的执行效率。尤其适合一些io密集的操作。例如需要经常进行网络操作,数据库访问的应用。

说明

        一个典型实现异步的方式则是通过多线程编程,你可以创建多个线程,并且一一启动他们。在多核环境下,每个线程都会 被分配到独立的核心上运行,实现真正的“并行”。

        但如果使用的是单核(单核心处理器),或者通过设置亲和力强制将线程绑定在某个核心上,操作系统则会通过时间片的方式来执行这些线程,只不过这些线程依然是在“并发”地执行。

        但像JavaScript编程语言,是没有多线程这个概念的。不过通过它的函数回调(function callback)机制,我们依然能够做到单线程“并发”。比如你可以通过多个fetch()同时访问多个网络资源。

        我们在调用fetch()时,代码并不会等待,而是继续执行。当获取到网络资源之后,回调函数才会被调用。 不过需要注意的是,虽然主程序核回调函数看起来像是同步 进行的,但是他们依然运行在同一个线程中。因此通过这种异步编程的操作,也可以完全能做到单线程的“并发”。

回调函数(Callback Functions)

        回调函数时在一个操作完成后执行的函数。例如,JavaScript中常见的异步函数如`setTimeout`和`fs.readFile`都接受一个回调函数,当操作完成时,回调函数被调用。

         回调函数实现异步编程的优点之一即是容易理解,但是7也有一个明星的缺点。如果我们需要一次执行多个异步操作,我们的程序将会编程下图这般。整个程序会一层接着一层嵌套下去,可读性就会变成非常发,这种情况也被称为“回调地狱”。

        为了解决这个问题,Promise应运而生,使用Promise的api,其中fetch()是一个很好的例子

Promise 

        Promise是一个对象,表示一个异步操作的最总完成(或失败)及其结果值。Promise具有三种状态:pending(进行中),fulfilled(已成功)和rejected(已失败)。Promise通过`then`、 `catch`和`finally`方法来处理异步操作的结果。

        以下是fetch()的举例说明

         如果请求成功完成,那么回调函数会被调起。请求的结果也会以参数的形式传递进来。但是光是这样,难么promise和回调函数就没有什么区别了。

        其实promise的优点在于可以用一种链式的结构将多个异步操作串联起来。比如这里的response.json()返回的也是一个promise,作用是讲返回的结果转成json。如果需要再转化完成后需要进行下一步的操作,那么可以在后面继续追加一个then。Promise的链式调用在一定程度上避免了代码的层层嵌套。

         在使用异步操作的时候,我们也有可能会遇到错误。比如各种网络问题或者返回的数据格式不正确等等。如果我们需要捕获这些错误。最简单的方法是附加一个catch在链式结构的末尾。如果之前的任意一个阶段发生了错误,那么catch将会被触发,而之后的then()将不会执行。这鱼同步编程中用到的try...catch块很相类似。

        类似的,promise还提供了finally(),他会在Promise链结束后调用,无论失败与否。比如我们遇到了加载动画,可以在finally中关闭它。

async/await

         async/await是Promise的语法糖,使得异步代码看起来像是同步代码。`async/await`函数总是返回一个Promise,`await`用于等待Promise完成,并 返回Promise的结果。它使得代码更易读,更易维护。但是需要注意的是:await看上去是需要暂停函数的执行,但是在等待过程中,js同样可以处理其他任务。比如更新界面,运行其他程序代码等等。因为await底层也是基于Promise和事件循环机制实现的。

使用:

  1. 首先我们需要将将async关键字将函数标记为异步函数(异步函数指的是返回值为Promise对象的函数),比如之前用到的fetch()就是一个异步函数。
  2. 在异步函数中,我们可以调用其他异步函数,不过我们不需要使用then(),而是使用一个更加简洁的await语法,await会等待Promise完成之后直接返回最终的结果。

最后呢,这里说明一下使用 async/await时的几个陷阱:

1.巧妙使用Promise.all()

        上面1的做法虽然不存在语法错误,但是这样写会打破两个fetch()操作的并行。因为这里会等到第一个任务完成之后再执行第二个任务。这里更高效的做法是将所有的Promise用Promise.all()组合起来,然后再去await。2写法的效率也会直接提升一倍。

2.forEach\map循环体中慎用async/await

        如果我们需要在循环体中执行异步操作,是不能直接调用forEach或者map这类方法的,尽管我们在回调中写了await,但是这里的forEach会立刻返回,它并不会等待所有的异步操作都执行完毕。如果我们需要等待循环中的异步操作都一一完成之后才继续执行,那么我们应该使用传统的for循环。

        更进一步,如果我们希望循环中的所有操作都并发执行,那么更好一点的写法是for await。这里的for循环依然会等到所有的异步操作都完成之后才会继续向后执行

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐