异步编程的前世今生


问题起源

  • 因为JS只有一根线程,因此异步编程对JS很重要。

目的

  • 异步编程的语法目标,就是怎样让它更像同步编程。

很久很久以前

异步编程的方法有四种:

  • 回调函数;
  • 事件监听;
  • 发布/订阅;
  • Promise对象。

回调函数的故事:

  • 概念
    JS对异步编程的实现就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。
  • 局限性
    回调函数本身没有问题,但是多个回调函数嵌套的时候就会有问题。会导致代码横向发展,而不是纵向发展。称之为回调地狱。
  • 解决之道
    为了解决该问题,Promise诞生了,它不是新的语法功能,而是一种新的写法,允许将回调函数的横向加载改为纵向加载。
  • 改进的地方
    Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。
  • 新面临的问题
    然后,Promise也面临一个巨大的问题,代码冗余,原来的任务被Promise包装一下,一眼看去都是一堆then,语义变的不清楚。

协程的故事

  • 概念
    传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做”协程”(coroutine),意思是多个线程互相协作,完成异步任务。

  • 本质
    协程的终极奥义就在于yield命令,它表示执行到此处,执行权将交给其他协程。即yield命令是异步两个阶段的分界线。

  • 协程在ES6中的衍生及其特点
    于是乎,Generator函数诞生了。它是协程在ES6中的实现,其最大特点就是可以交出函数的执行权,即暂停执行。整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。

  • Generator的另外两个特性
    Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。

  • 函数体内外数据交换
    next 方法返回值的 value 属性,是 Generator 函数向外输出数据;next 方法还可以接受参数,这是向 Generator 函数体内输入数据。

  • 错误处理机制
    Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。

  • Generator函数的局限:

    • 不能自动执行

    • 程序的执行权移出以后,需要再次交换回来。

综上所述:Generator 函数就是一个异步操作的容器。它的自动执行需要一种机制(即自动执行器),当异步操作有了结果,能够自动交回执行权

  • 两种解决之道
    由于Generator函数中的yield 命令用于将程序的执行权移出 Generator 函数,那么就需要一种方法,将执行权再交还给 Generator 函数。

    • 基于Thunk函数的自动执行器:回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

    • 基于Promise对象的自动执行器:Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。

  • co函数库,用于Generator函数的自动执行。

    它可以使你不用编写Generator函数的执行器。其原理就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个库。使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。

异步编程的终极解决之道:async函数

  • 概念
    一句话,async函数就是Generator函数的语法糖。

  • 改进点
    async函数对Generator函数的改进体现在以下几点:

    • 内置执行器
      Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。

    • 更好的语义
      async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。

    • 更广的适用性
      co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。所以,async函数的实现,就是把Generator函数和自动执行器包装在一个函数里。

    • 返回值是Promise
      async函数的返回值是 Promise 对象。

  • 使用手册说明

    • await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try…catch中。
    • await命令只能用在async函数中,如果用在普通函数中,就会报错。
    • 如有多个请求并发执行,请使用Promise.all方法。

参考链接