JavaScript - 异步编程

147 阅读55分钟

前言

JavaScript 的执行环境是 "单线程"(single thread),所谓"单线程",单线程就意味着一次只能完成一件任务,若有多个任务就必须排队,前面一个任务完成再执行后面一个任务,以此类推

注意:所谓的单线程是指执行代码的那个线程是单线程,但浏览器不是单线程的,JS 调用的某些内部的 API 不是单线程的,如 setTimeout 等是有专门的内部线程去负责处理,时间到了将回调函数放到消息队列等待执行

采用单线程模式工作的原因与它最早的设计初衷有关

  • 最早 JS 就是运行在浏览器端的脚本语言,目的是为了实现页面上的动态交互,而实现交互的核心就是 DOM 操作,这就决定了必须使用单线程模式,否则容易出现复杂的线程同步问题
  • 假定在 JS 中有多个线程同时工作,其中一个线程修改某个 DOM 元素,而另个线程同时删除该元素,此时浏览器无法明确以哪个线程为准
  • 所以干脆设计成一个单线程,哪怕 HTML5 出现的 web worker 也是不允许操作 DOM 结构,可以完成一些分布式的计算

单线程模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 JS 代码长时间运行(如死循环)导致整个页面卡在这个地方,其他任务无法执行

为了解决这个问题,JS 语言将任务的执行模式分成两种:同步(Synchronous)异步(Asynchronous)

  • 同步:指代码中的任务依次执行,执行顺序与代码的编写顺序一致。在单线程情况下,大多数任务是以同步模式去执行(同步不是指同时执行,而是指排队依次执行),缺点就是当某个任务或代码执行时间过长时就会导致后面的任务延迟,导致阻塞(如界面卡顿等)(同步是阻塞的
  • 异步:不会等待当前任务结束才会开始下一个任务。对于耗时操作,都是开启过后立即往后执行下个任务,耗时操作的后续逻辑一般会通过回调函数的方式定义,当耗时任务完成后就会自动执行传入的回调函数,所以程序的执行顺序与任务的排列顺序是不一致的、异步的(异步是非阻塞的),若无异步模式,单线程的 JS 就无法同时处理大量的耗时任务

同步异步不是指写代码的方式,而是指运行环境提供的 API 是以同步异步模式的方式工作

  • 同步模式的 API 的特点是这个任务执行完代码才会继续往下走,如 console.log()
  • 异步模式的 API 是下达这个任务开启的指令后就会继续往下执行,不会在这行等待任务的结束

异步模式非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是 Ajax 操作。在服务器端异步模式甚至是唯一的模式,因为执行环境单线程的,若允许同步执行所有 http 请求,服务器性能会急剧下降,很快就会失去响应

本文总结了异步模式编程的几种方法,理解它们可以让我们写出结构更合理、性能更出色、维护更方便的 JS 程序

回调函数(Callback)

这是异步编程最基本的方法,所有异步编程方案的根基都是回调函数

简单的说回调函数就是当某些操作完成后触发处理某些功能的逻辑函数

ajax(url, () => { // 处理逻辑 })

采用这种方式可以把同步操作变成了异步操作,处理逻辑不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行

  • 回调函数常用应用场合

    • 资源加载:动态加载 js 文件后执行回调、加载 iframe 后执行回调、ajax 操作回调、图片加载完成执行回调等

    • DOM 事件及 NodeJs 事件基于回调机制(NodeJs 回调可能会出现多层回调嵌套的问题)

    • 链式调用链式调用时在赋值器(setter)方法中(或本身没有返回值的方法中)很容易实现链式调用,而取值器(getter)相对来说不好实现链式调用,因为需要取值器返回需要的数据而不是 this 指针,若要实现链式方法,可以用回调函数来实现

    • setTimeoutsetInterval 函数调用得到其返回值。由于两个函数都是异步的,即:它们的调用时序和程序的主流程是相对独立的,所以没有办法在主体里等待它们的返回值,它们被打开时程序也不会停下来等待,否则也就失去了 setTimeoutsetInterval 的意义,所以用 return 已经没有意义,只能使用 callbackcallback 的意义在于将 timer 执行的结果通知给相应逻辑函数进行及时处理

  • 回调函数优缺点

    回调函数有个致命弱点就是容易写出回调地狱(Callback hell),假设多个请求存在依赖性,可能就会写出如下代码

    ajax(url, () => {
      // 处理逻辑
      ajax(url1, () => {
        // 处理逻辑
        ajax(url2, () => {
          // 处理逻辑
        })
      })
    })
    

    回调地狱的根本问题是:

    • 嵌套函数存在耦合性,一旦有所改动就会牵一发而动全身
    • 嵌套函数一多,就很难处理错误
    • 回调嵌套很多时,代码就会非常繁琐且不利于维护和阅读,会给编程带来很多的麻烦

    回调函数的优点:简单、容易理解和实现

    缺点:不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),且每个任务只能指定一个回调函数。此外它不能使用 try catch 捕获错误和不能直接 return

事件监听

采用事件驱动模式,任务的执行不取决于代码的顺序,而取决于某个事件是否发生

下面 fn1fn2,要求 fn2 必须等到 fn1 执行完成才能执行。首先,为 fn1 绑定一个事件(这里采用的 jQuery 的写法)

fn1.on('done', fn2);

上面这行代码的意思是当 fn1 发生 done 事件,就执行 fn2,然后对 fn1 进行改写

function fn1(){
  setTimeout(function() {
    // fn1 的任务代码
    fn1.trigger('done');
  }, 1000);
}

fn1.trigger('done') 表示执行完成后立即触发 done 事件,从而开始执行 fn2

这种方法的优点:比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数且可以"去耦合"(Decoupling),有利于实现模块化

缺点:整个程序都要变成事件驱动型,运行流程会变得很不清晰

发布/订阅

假定存在一个"信号中心",某个任务执行完成就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行,即发布/订阅模式(publish-subscribe pattern),又称观察者模式(observer pattern)

首先,fn2 向信号中心 jQuery 订阅 done 信号

jQuery.subscribe("done", fn2);

然后,fn1 进行如下改写

function f1() {
  setTimeout(function () {
    // f1的任务代码
    jQuery.publish("done");
  }, 1000);
}

jQuery.publish("done") 的意思是 fn1 执行完成后向"信号中心" jQuery 发布 done 信号,从而引发 fn2 的执行

此外,fn2 完成执行后也可以取消订阅(unsubscribe)

jQuery.unsubscribe("done", f2);

这种方法的性质与事件监听类似,但是明显优于后者。因为可以通过查看"消息中心" 了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行

Promise

  • 什么是 promise?

    Promise 是异步编程的一种解决方案,比传统的回调函数事件解决方案更合理和更强大,它由 CommonJS 社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象

    所谓 Promise 其实就是一个对象,用来表示一个异步任务最终结束过后究竟是成功还是失败,就是内部对外部的一个“承诺”,承诺过一段时间会给出一个结果。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理

    Promise 对象有以下两个特点:

    • 对象的状态不受外界影响

      Promise 对象代表一个异步操作,有三种状态:pending(等待)、fulfilled(成功)和 rejected(失败)。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态

    • 状态一旦改变,就不会再变,任何时候都是得到这个结果

      Promise 对象的状态改变,只有两种可能:从 pending -> fulfilled 和从 pending -> rejected。只要这两种情况发生,状态就不会再变了,会一直保持这个结果

    只要状态改变已经发生,再对 Promise 对象添加回调函数也会立即得到这个结果(这与事件(Event)完全不同,事件的特点是:若错过了再去监听是得不到结果的)。当成功或者失败确定后会有对应的任务自动执行,分别对应:onFulfilledonRejected

    let p = new Promise((resolve, reject) => {
      reject('reject');
      resolve('success');
    });
    
    p.then(value => {
      console.log(value);
    }, reason => {
      console.log(reason);
    })
    // reject
    

    1、当构造 Promise 时构造函数内部的代码是立即执行
    2、平时用的很多库或插件都运用了 Promise,如 axiosfetch

    有了 Promise 对象就可以将异步操作同步操作的流程表达出来,避免了层层嵌套的回调函数。此外 Promise 对象提供统一的接口,使得控制异步操作更加容易

    Promise 也有一些缺点:

    • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消
    • 若不设置回调函数Promise 内部会抛出的错误,不会反应到外部
    • 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
  • Promise 解决什么?

    • 回调地狱,代码难以维护,常常第一个函数的输出是第二个函数的输入等现象
    • Promise 可以支持多个并发的请求,获取并发请求中的数据
    • Promise 可以解决异步的问题,本身不能说 Promise异步
    • 回调调用次数过多/少、早/晚、不调等问题
  • Promise 的用法

    ES6 规定 Promise 对象是一个构造函数,用来生成 Promise 实例。Promise 身上有 allrejectresolve 等方法,原型上有 thencatch 等方法

    Promise 的构造函数接收一个函数作为参数且这个函数需要传入两个参数(它们是两个函数,由 JS 引擎提供,不用自己部署)

    • resolve:将 Promise 对象的状态从 pending 变为 fulfilled,在异步操作成功时调用并将异步操作的结果作为参数传递出去
    • reject:将 Promise 对象的状态从 pending 变为 rejected,在异步操作失败时调用并将异步操作报出的错误作为参数传递出去

    基本用法

    const promise = new Promise(function(resolve, reject) {
      // resolve(100); 
      reject(new Error('promise rejected'));
    });
    promise.then(function(value) {
      console.log('resolved', value);
    }, function(error) {
      console.log('rejected', error);
    })
    

从表面上看 Promise 只是能够简化层层回调的写法,而实质上 Promise 的精髓是状态,用维护状态传递状态的方式来使得回调函数能够及时调用,它比传递 callback 函数要简单、灵活的多

  • then 方法

    Promise 实例生成以后可以用 then 方法分别指定 fulfilled 状态和 rejected 状态的回调函数,then 方法是定义在原型对象 Promise.prototype 上的

    • then 方法里有两个参数:onFulfilledonRejected(成功有成功的值,失败有失败的原因)(它们都是可选的)。若它们是函数则必须分别在当状态 state 变为 fulfilled、状态 staterejected 后被调用,valuereason 依次作为它们的第一个参数

    • then 方法返回的是一个新的 Promise 实例(不是原来那个 Promise 实例),因此可以采用链式写法,即 then 方法后面再调用另一个 then 方法(相比传统回调函数的方式,Promise 最大的优势就是可以链式调用,这样可以最大程度的避免回调函数嵌套

    • 前面 then 方法中回调函数的返回值会作为后面 then 方法回调的参数,若 then 回调函数中返回一个新 Promise 对象,后个 then 就是为这个返回的新 Promise 对象去添加状态明确后的回调,该回调会等待返回的新 Promise 对象状态变化确定后才会调用(若变为 fulfilled 就调用第一个回调函数,若状态变为 rejected,就调用第二个回调函数

    • 若返回一个普通的值,该值就作为 then 方法返回的 Promise 的值,下个 then 方法接收的回调函数参数就是该值;若没有返回任何值,就默认返回一个 undefined

    • then 中若使用了 return,则 return 的值会被 Promise.resolve() 包装

    • then 中可以不传递参数,若不传递会透到下一个 then

      Promise.resolve(1).then(res => {
        console.log(res); // 1
        return 2;
      }).catch(err => 3).then(res => {
        console.log(res); // 2
      })
      
      Promise.resolve(1)
        .then(x => x + 1)
        .then(x => {
          throw new Error('my error');
        })
        .catch(() => 1).then(x => x + 1)
        .then(x => console.log(x);) // 2
        .catch(console.error)
      

    一般来说,调用 resolverejectPromise 的使命就完成了,后继操作应该放到 then 等方法里,而不应该直接写在 resolvereject 的后面。所以最好在它们前面加上 return 语句,这样就不会有意外

    new Promise((resolve, reject) => {
      return resolve(1);
      // 后面的语句不会执行
      console.log(2);
    })
    
  • 异常处理

    Promise.prototype.catch() 方法是 then(null, rejection)then(undefined, rejection) 的别名,用于指定发生错误时的回调函数。catch 方法注册失败回调在链式调用中更常见

    getJSON('/posts.json').then(function(posts) {
      // ...
    }).catch(function(error) {
      // 处理 getJSON 和 前一个回调函数运行时发生的错误
      console.log('发生错误!', error);
    });
    

    上面代码中 getJSON() 方法返回一个 Promise 对象,若该对象状态变为 fulfilled,则会调用 then() 方法指定的回调函数;若异步操作抛出错误,状态就会变为 rejected,就会调用 catch() 方法指定的回调函数,处理这个错误

    注意:then()方法指定的回调函数,若运行中抛出错误也会被 catch() 方法捕获

    Promise 抛出一个错误,就被 catch() 方法指定的回调函数捕获

    const promise = new Promise(function(resolve, reject) {
      throw new Error('test');
    });
    promise.catch(function(error) {
      console.log(error);
    });
    // Error: test
    
    // 等价于下面两种写法
    // 写法一
    const promise = new Promise(function(resolve, reject) {
      try {
        throw new Error('test');
      } catch(e) {
        reject(e);
      }
    });
    promise.catch(function(error) {
      console.log(error);
    });
    
    // 写法二
    const promise = new Promise(function(resolve, reject) {
      reject(new Error('test'));
    });
    promise.catch(function(error) {
      console.log(error);
    });
    

    Promise 状态已经变成 resolved,再抛出错误是无效的

    const promise = new Promise(function(resolve, reject) {
      resolve('ok');
      throw new Error('test');
    });
    promise
      .then(function(value) { console.log(value) })
      .catch(function(error) { console.log(error) });
    // ok
    

    Promise 对象的错误具有冒泡性质,会一直向后传递,直到被捕获为止。即错误总是会被下一个 catch 语句捕获

    // 一共有三个 Promise 对象:一个由 getJSON() 产生,两个由 then() 产生
    // 它们之中任何一个抛出的错误都会被最后一个 catch() 捕获
    getJSON('/post/1.json').then(function(post) {
      return getJSON(post.commentURL);
    }).then(function(comments) {
      ...
    }).catch(function(error) {
      // 处理前面三个 Promise 产生的错误
    });
    

    一般来说,不要在 then() 方法里面定义 reject 状态的回调函数(即 then 的第二个参数),建议使用 catch 方法

    下面第二种写法要好于第一种写法,理由是第二种写法还可以捕获前面 then 方法执行中的错误,也更接近同步的写法(try/catch)。因此建议总是使用 catch() 方法,而不使用 then() 方法的第二个参数,而且若 then 中返回一个 Promise 且该 Promise 中又存在异常,该 then 方法第二个参数的失败回调是捕获不到该异常的

    // bad
    promise.then(function(data) {
      // success
    }, function(err) {
      // error
    });
    
    // good
    promise
      .then(function(data) { //cb
        // success
      })
      .catch(function(err) {
        // error
      });
    

    跟传统的 try/catch 代码块不同的是,若没有使用 catch() 方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应

    const asyncExample = function() {
      return new Promise(function(resolve, reject) {
        // 下面一行会报错,因为 x 没有声明
        resolve(x + 2);
      });
    };
    
    asyncExample().then(function() {
      console.log('!!!');
    });
    
    setTimeout(() => { console.log(123); }, 2000);
    // Uncaught (in promise) ReferenceError: x is not defined
    // 123
    

    上面代码中,asyncExample() 函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行时会打印出错误提示,但是不会退出进程、终止脚本执行,2 秒后还是会输出 123,即 Promise 内部的错误不会影响到 Promise 外部的代码

    再看下面的例子

    const promise = new Promise(function (resolve, reject) {
      resolve('ok');
      setTimeout(function () { throw new Error('test') }, 0)
    });
    promise.then(function (value) { console.log(value) });
    // ok
    // Uncaught Error: test
    

    上面代码中,Promise 指定在下一轮事件循环再抛出错误,到了那时 Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误

    一般总是建议,Promise 对象后面要跟 catch() 方法,这样可以处理 Promise 内部发生的错误。catch() 方法返回的还是一个 Promise 对象,因此后面还可以接着调用 then() 方法

    const asyncExample = function() {
      return new Promise(function(resolve, reject) {
        // 下面一行会报错,因为 x 没有声明
        resolve(x + 2);
      });
    };
    
    asyncExample()
      .catch(function(error) {
        console.log('oh no', error);
      })
     .then(function() {
        console.log('carry on');
      });
      
    // oh no ReferenceError: x is not defined
    // carry on
    

    若没有报错则会跳过 catch() 方法

    Promise.resolve()
      .catch(function(error) {
        console.log('oh no', error);
      })
      .then(function() {
         console.log('carry on');
      });
    
    // carry on
    

    上面的代码因为没有报错,跳过了 catch() 方法,直接执行后面的 then() 方法。此时要是 then() 方法里面报错,就与前面的 catch() 无关了

    catch() 方法之中,还能再抛出错误

    在执行 resolve 的回调(即 then 中的第一个参数)时,若抛出异常(代码出错等)则并不会报错卡死 JS,而是会进到这个 catch 方法中,即进到 catch 方法里去且把错误原因传到了 reason 参数中,即便是有错误的代码也不会报错了,这与 try/catch 语句有相同的功能

    // 在resolve的回调中,console.log(somedata) 时 somedata 这个变量是没有被定义的
    // 如果不用 Promise,代码运行到这里就直接在控制台报错了,不往下运行了
    // 但是在这里,会得到下面的结果
    p.then((data) => {
      console.log('resolved', data);
      console.log(somedata); //此处的 somedata 未定义
    }).catch((err) => {
      console.log('rejected', err);
    }); 
    // resolved 4
    // rejected ReferenceError: somedata is not defined
    
  • finally 方法

    finally 是原型上的方法 Promise.prototype.finally()

    finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作,该方法是 ES2018 引入标准的

    promise
      .then(result => {···})
      .catch(error => {···})
      .finally(() => {···});
    

    上面代码中,不管 Promise 最后的状态,执行完 thencatch 指定的回调函数后都会执行 finally 方法指定的回调函数

    finally 方法的回调函数不接受任何参数,这意味着没有办法知道 Promise 状态到底是 fulfilled 还是 rejected,即 finally 方法里的操作应该是与状态无关的,不依赖于 Promise 的执行结果

    finally 本质上是 then 方法的特例

    promise.finally(() => {...});
    
    // 等同于
    promise.then(result => {
      ...
      return result;
    }, error => {
      ...
      throw error;
    });
    

    上面代码中,若不使用 finally 方法,同样的语句需要为成功和失败两种情况各写一次,有了 finally 方法,则只需要写一次

    finally 方法总是会返回原来的值

    // Promise {<fulfilled>: undefined}
    Promise.resolve(2).then(() => {}, () => {});
    
    // Promise {<fulfilled>: 2}
    Promise.resolve(2).finally(() => {})
    
    // Promise {<fulfilled>: undefined}
    Promise.reject(3).then(() => {}, () => {})
    
    // Promise {<rejected>: 3}
    Promise.reject(3).finally(() => {})
    
  • 静态方法

    • Promise.resolve():快速把一个值转换成 Promise 对象

      • 若接收的是另一个 Promise 对象,则该对象会原封不动的返回

      • 若传入的是一个有跟 Promise 一样的 then 方法(在该方法中可以接收 onFulfilledonRejected 的两个回调)的对象,当调用 onFulfilled 传入值,则这样的对象也可以作为 Promise 对象被执行,在后面的 then 方法也可以拿到对应传入的值。带有 then 方法的对象称为实现了 thenable 的接口(可以被 then 的对象)(thenable 对象指的是具有 then 方法的对象)

      Promise.resolve('foo').then(function(value) {
        console.log(value);
      })
      
      // 上面的与下同等
      new Promise(function(resolve, reject) {
        resolve('foo');
      })
      
      const promise1 = ajax('xxx');
      const promise2 = Promise.resolve(promise1);   
      console.log(promise1 === promise2); // true 
      
      // Promise.resolve() 方法会将 thenable 对象转为 Promise 对象
      let thenable = {
        then: function(resolve, reject) {
          resolve(42);
        }
      };
      
      let p1 = Promise.resolve(thenable);
      p1.then(function (value) {
        console.log(value);  // 42
      });
      
      • 参数不是具有 then() 方法的对象或根本就不是对象:若参数是一个原始值或是一个不具有 then() 方法的对象,则 Promise.resolve() 方法返回一个新的 Promise 对象,状态为 resolved
      const a = Promise.resolve('hhh');
      a.then(function (value) {
        console.log(value); // hhh
      });
      

      上面代码生成一个新的 Promise 对象的实例 a。由于字符串 hhh 不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是 resolved,所以回调函数会立即执行 Promise.resolve() 方法的参数,会同时传给回调函数

      Promise.resolve() 方法允许调用时不带参数,直接返回一个 resolved 状态的 Promise 对象,所以若希望得到一个 Promise 对象,比较方便的方法就是直接调用 Promise.resolve() 方法

      const a = Promise.resolve();
      a.then(function () {...});
      

      需要注意的是,立即 resolve()Promise 对象,是在本轮事件循环(event loop)的结束时执行,而不是在下一轮事件循环的开始时

      setTimeout(function () {
        console.log('three');
      }, 0);
      
      Promise.resolve().then(function () {
        console.log('two');
      });
      
      console.log('one');
      // one
      // two
      // three
      

      上面代码中,setTimeout(fn, 0) 在下一轮事件循环开始时执行,Promise.resolve() 在本轮事件循环结束时执行,console.log('one') 则是立即执行,因此最先输出

    • Promise.reject():快速创建一个表示失败的 Promise 对象,该实例的状态为 rejected

      const a = Promise.reject('wrong');
      // 等同于
      const a = new Promise((resolve, reject) => reject('wrong'))
      
      a.then(null, function (v) {
        console.log(v);
      });
      // wrong
      

      Promise.reject() 方法的参数会原封不动地作为 rejectreason,变成后续方法的参数

      Promise.reject('wrong')
       .catch(e => {
         console.log(e);
       })
      // wrong
      
  • 并行执行

    • Promise.all():接受一个数组作为参数(参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例),数组每个元素都是一个 Promise 对象(若不是则会先调用 Promise.resolve 方法,将参数转为 Promise 实例后再进一步处理),返回一个全新的 Promise 对象(这是一个很好的同步执行多个异步任务的方式,它们完成顺序不重要,但必须都要完成)

      • 当参数内部所有的 Promise 都完成后,返回的全新的 Promise 才会完成

      • 内部所有的 Promise 状态都变成 fulfilled,返回的新的 Promise 才会变成 fulfilled,此时返回值组成一个数组,数组包含每个异步任务执行的结果

      • 若其中有某个任务失败,则返回的新的 Promise 的状态为 rejected,此时返回值是第一个被 reject 的实例

      • 就本质而言列表中的每个值都会通过 Promise.resolve 过滤,以确保要等待的是一个真正的 Promise若数组为空则主 Promise 会立即执行

      • 有了 all 就可以并行执行多个异步操作且在一个回调中处理所有的返回数据。有一个场景很适合用:一些游戏类的素材比较多的应用,打开网页时预先加载需要用到的各种资源如图片、flash 及各种静态文件,所有的都加载完后再进行页面的初始化

      // 例 1
      let Promise1 = new Promise(function(resolve, reject) {resolve('111')});
      let Promise2 = new Promise(function(resolve, reject){resolve('222')});
      let Promise3 = new Promise(function(resolve, reject){resolve('333')});
      let p = Promise.all([Promise1, Promise2, Promise3]);
      p.then(value => { 
        console.log('success:', value);
      }, err => { 
       console.log('error:', err);
      })
      // success: ['111', '222', '333']
      
      // 例 2
      let Promise1 = new Promise(function(resolve, reject){resolve('111')});
      let Promise2 = new Promise(function(resolve, reject){resolve('222')});
      let Promise3 = new Promise(function(resolve, reject){reject('333')});
      let p = Promise.all([Promise1, Promise2, Promise3]);
      p.then(value => { 
        console.log('success:', value)
      }, err => { 
        console.log('error:', err)
      })
      // error: 333
      

      注意,若作为参数的 Promise 实例自己定义了 catch 方法,则它一旦被 rejected,并不会触发 Promise.all()catch 方法

      var test1 = new Promise((resolve, reject) => {
        resolve('hello');
      }).then(result => result).catch(e => e);
      
      var test2 = new Promise((resolve, reject) => {
        throw new Error('报错了');
      }).then(result => result).catch(e => e);
      
      Promise.all([test1, test2])
      .then(result => console.log(result))
      .catch(e => console.log(e));
      // ['hello', Error: 报错了]
      

    上面代码中,test1resolvedtest2 首先会 rejected,但是 test2 有自己的 catch 方法,该方法返回的是一个新的 Promise 实例,test2 指向的实际上是这个实例。该实例执行完 catch 方法后,也会变成 resolved,导致 Promise.all() 方法参数里面的两个实例都会 resolved,因此会调用 then 方法指定的回调函数,而不会调用 catch 方法指定的回调函数

    test2 没有自己的 catch 方法,就会调用 Promise.all()catch 方法

    var test1 = new Promise((resolve, reject) => {
      resolve('hello');
    }).then(result => result).catch(e => e);
    
    var test22 = new Promise((resolve, reject) => {
      throw new Error('报错了');
    }).then(result => result);
    
    Promise.all([test1, test22])
    .then(result => console.log(result))
    .catch(e => console.log(e));
    // Error: 报错了
    
  • Promise.race()Promise.race() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。只要其中某个任务完成了(状态改变),所返回的 Promise 对象就完成结束了(谁跑的快,以谁为准执行回调)

    • Promise.race() 方法的参数与 Promise.all() 方法一样,若不是 Promise 实例则会先调用 Promise.resolve() 方法,将参数转为 Promise 实例,再进一步处理

    • 一旦有任何一个 Promise 决议完成,Promise.race([...]) 就会完成;一旦有任何一个 Promise 决议结果是 rejected,则结果的状态就是 rejected

    • 传入一个空数组则 Promise.race([...]) 永远不会决议,而不是立即决议(注意:不要传空数组

    • 因为只有一个 Promise 能取胜,所以完成值是单个消息,而不是像 Promise.all([...]) 那样可能是个数组

    • Promise.race() 的使用场景:如可以用 Promise.race() 给某个异步请求设置超时时间且在超时后执行相应的操作

    // requestImg 函数会异步请求一张图片,把地址写为"图片的路径",所以肯定是无法成功请求到的
    // timeout 函数是一个延时 5 秒的异步操作
    // 把这两个返回 Promise 对象的函数放进 race,于是他俩就会赛跑,若 5 秒之内图片请求成功了则会进入 then 方法,执行正常的流程
    // 若 5 秒钟图片还未成功返回则 timeout 就跑赢了,则进入 catch,报出“图片请求超时”的信息
    
    // 请求某个图片资源
    function requestImg(){
      var p = new Promise((resolve, reject) => {
        var img = new Image();
        img.onload = function(){
          resolve(img);
        };
        img.src = '图片的路径';
      });
      return p;
    }
    // 延时函数,用于给请求计时
    function timeout(){
      var p = new Promise((resolve, reject) => {
        setTimeout(() => {
          reject('图片请求超时');
        }, 5000);
      });
      return p;
    }
    
    Promise.race([requestImg(), timeout()]).then((data) => { 
      console.log(data);
    }).catch((err) => {
      console.log(err);
    });
    
  • Promise.allSettled()

    ES2020 引入了 Promise.allSettled() 方法,用来确定一组异步操作是否都结束了(不管成功或失败),包含了 fulfilledrejected 两种情况

    Promise.allSettled() 方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是 fulfilled 还是 rejected),返回的 Promise 对象才会发生状态变更

    该方法返回的新的 Promise 实例,一旦发生状态变更,状态总是fulfilled,不会变成 rejected。状态变成 fulfilled 后,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个 Promise 对象

    var resolved = Promise.resolve(1);
    var rejected = Promise.reject(2);
    
    var allSettledPromise = Promise.allSettled([resolved, rejected]);
    
    allSettledPromise.then(function (results) {
      console.log(results);
    });
    // [{status: 'fulfilled', value: 1}, {status: 'rejected', reason: 2}]
    

    上面代码中,Promise.allSettled() 的返回值 allSettledPromise,状态只可能变成 fulfilled。它的回调函数接收到的参数是数组 results,该数组的每个成员都是一个对象,对应传入 Promise.allSettled() 的数组里面的两个 Promise 对象的异步操作的结果

    成员对象的 status 属性的值只可能是字符串 fulfilled 或字符串 rejected,用来区分异步操作是成功还是失败。若是 fulfilled,对象会有 value 属性,若是 rejected,会有 reason 属性,对应两种状态时前面异步操作的返回值

  • Promise.any()

    ES2021 引入了 Promise.any() 方法,该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回

    Promise.any([
      fetch('https://v8.dev/').then(() => 'home'),
      fetch('https://v8.dev/blog').then(() => 'blog'),
      fetch('https://v8.dev/docs').then(() => 'docs')
    ]).then((first) => {  // 只要有一个 fetch() 请求成功
     console.log(first);
    }).catch((error) => { // 所有三个 fetch() 全部请求失败
     console.log(error);
    });
    

    只要参数实例有一个变成 fulfilled 状态,包装实例就会变成 fulfilled 状态;若所有参数实例都变成 rejected 状态,包装实例就会变成 rejected 状态

    Promise.any()Promise.race() 方法很像,只有一点不同,就是 Promise.any() 不会因为某个 Promise 变成 rejected 状态而结束,必须等到所有参数 Promise 变成 rejected 状态才会结束

    Promise.any() 抛出的错误,不是一个一般的 Error 错误对象,而是一个 AggregateError 实例,它相当于一个数组,每个成员对应一个被 rejected 的操作所抛出的错误,下面是 AggregateError 的实现示例

    // new AggregateError() extends Array
    const err = new AggregateError();
    err.push(new Error("first error"));
    err.push(new Error("second error"));
    // ...
    throw err;
    
    var resolved = Promise.resolve(1);
    var rejected = Promise.reject(2);
    var alsoRejected = Promise.reject(Infinity);
    
    Promise.any([resolved, rejected, alsoRejected]).then(function (result) {
      console.log(result); // 1
    });
    
    Promise.any([rejected, alsoRejected]).catch(function (results) {
      console.log(results); // AggregateError: All promises were rejected
    });
    
  • 执行时序

    Promise 的回调会作为微任务执行,会在本轮任务结束末尾执行

    宏任务在执行过程中可以临时加上一些额外需求,可以选择作为一个新的宏任务进到队列中排队

    绝大多数异步调用 API 都可作为宏任务进入到回调队列,PromiseMutationObserverprocess.nextTick(node) 等会作为微任务

  • Promise 相关实现

    • Promise 的声明:

      Promise 是一个类,这里用 class 来声明

      由于new Promise((resolve, reject)=>{}),在执行这个类时需要传递一个执行器(回调函数),执行器立即执行,秘籍里称为 executorexecutor 是立即执行

      executor 里有两个参数,分别是 resolve(成功)reject(失败)

      由于 resolvereject 可执行,所以都是函数,这里用 let 声明

      class MyPromise {
        // 构造器
        constructor(executor) {
          // 立即执行
          executor(this.resolve, this.reject);
        }
      
        // 成功
        resolve = () => {}; 
        // 失败
        reject = () => {};
      }
      

      Promise 中有三种状态:

      • 等待(pending)
      • 成功(fulfilled)
      • 失败(rejected)

      一旦状态确定就不可更改(等待 -> 成功、等待 -> 失败)

      resolvereject 函数是用来更改状态的

      • resolve -> fulfilled
      • reject -> rejected
      // 创建三个常量用于表示状态,对于经常使用的一些值都应该通过常量来管理,便于开发及后期维护 
      const PENDING = 'pending'; // 等待 
      const FULFILLED = 'fulfilled'; // 成功 
      const REJECTED = 'rejected'; // 失败
      
      class MyPromise {
        constructor(executor) {
          // 捕获执行器错误
          // 若 executor 执行报错,则直接执行reject
          try {
            executor(this.resolve, this.reject);
          } catch (e) {
            this.reject(e);
          }
        }
      
        // 初始化 state 为等待状态
        state = PENDING;
        // 成功的值
        value = undefined;
        // 失败的原因
        reason = undefined;
      
        resolve = value => {
          // 若状态不是等待,则阻止程序向下执行
          if (this.status !== PENDING) return;
          // 将状态改为成功
          this.status = FULFILLED; 
          // 保存成功后的值 
          this.value = value;
        };
      
        reject = reason => {
          // 若状态不是等待,则阻止程序向下执行 
          if (this.state === PENDING) return;
          // 将状态改为成功
          this.status = REJECTED; 
          // 保存成功后的值 
          this.reason = reason;
        };
      }
      
    • then 方法

      then 方法内部做的事情就是判断状态:若成功(state 为 fulfilled)则调用成功的回调函数,传入对应的 value 参数;相反则是调用失败(state 为 rejected)的回调函数,传入对应的 reason 参数

      then 方法是被定义在原型对象中的

      class MyPromise {
        constructor(executor) { ... }
        
        // ...
        
        then(onFulfilled, onRejected) {
          // 判断状态
          if (this.status === FULFILLED) { 
            onFulfilled(this.value);
          } else if (this.status === REJECTED) {
            onRejected(this.reason);
          }
        }
      }
      

      加入异步逻辑

      • 当在执行器中加入异步代码时,由于异步代码没有立即执行,且异步代码中调用 resolvereject 时,而 then 是立即执行的,此时的状态是等待状态,若是等待状态此时并不知道是成功还是失败,因此此处得增加判断以及相应处理

      • then 调用时将成功和失败存到各自的数组,当异步代码执行完成调用 resolvereject 时,此时才去执行成功或失败对应的回调函数

      • 此处增加实现 then 方法多次调用添加多个处理函数

      const PENDING = 'pending'; // 等待 
      const FULFILLED = 'fulfilled'; // 成功 
      const REJECTED = 'rejected'; // 失败
      
      class MyPromise {
        constructor(executor) {
          // 捕获执行器错误
          // 若 executor 执行报错,则直接执行reject
          try {
            executor(this.resolve, this.reject);
          } catch (e) {
            this.reject(e);
          }
        }
        // 初始化 state 为等待状态
        state = PENDING;
        // 成功的值
        value = undefined;
        // 失败的原因
        reason = undefined;
         
        // 成功回调存放
        successCallback = []; 
        // 失败回调存放
        failCallback = [];
      
        resolve = value => {
          // 若状态不是等待,则阻止程序向下执行
          if (this.status !== PENDING) return;
          // 将状态改为成功
          this.status = FULFILLED; 
          // 保存成功后的值 
          this.value = value;
          // 成功回调是否存在,若存在则调用 
          // this.successCallback && this.successCallback(this.value); 
          
          // 从前到后执行回调 
          // Array.shift() 取出数组第一个元素,然后()调用,shift 操作后,数组将删除该元素,直到数组为空
          while (this.successCallback.length) this.successCallback.shift()(this.value);
        };
      
        reject = reason => {
          // 若状态不是等待,则阻止程序向下执行 
          if (this.state === PENDING) return;
          // 将状态改为成功
          this.status = REJECTED; 
          // 保存成功后的值 
          this.reason = reason;
          // 成功回调是否存在,若存在则调用 
          // this.failCallback && this.failCallback(this.value) 
         
          // 从前到后执行回调
          // Array.shift() 取出数组第一个元素,然后()调用,shift 操作后,数组将删除该元素,直到数组为空
          while (this.failCallback.length) this.failCallback.shift()(this.reason);
        };
         
        then(onFulfilled, onRejected) {
          if (this.status === FULFILLED) {
            onFulfilled(this.value);
          } else if (this.status === REJECTED) {
            onRejected(this.reason);
          } else {
            // 等待状态 
            // 将成功和失败回调先存储起来    
            this.successCallback.push(onFulfilled);   
            this.failCallback.push(onRejected);
          }
        }
      }
      

      解决链式调用

      • 后面的 then 的参数值取决于前面的 then 的回调的返回值

      • then 要实现链式调用的前提是每次 then 方法都返回一个 Promise 对象

      • 秘籍 规定 onFulfilled()onRejected() 的值,即第一个 then 返回的值,叫做 x,判断 x 的函数叫做 resolvePromise

        (1)首先,要看 x 是不是 Promise,若是 promise 则取它的结果,作为新的 promise2 成功的结果;若是普通值,则直接作为 promise2 成功的结果

        (4)所以要比较 xpromise2

        (5)resolvePromise 的参数有 promise2(默认返回的 Promise)、xreturn的对象)、resolvereject

        (6)resolverejectpromise2

      class MyPromise {
        constructor(executor) {...}
        // ...
        
        then(onFulfilled, onRejected) {
          let promise2 = new MyPromise((resolve, reject) => {
            if (this.status === FULFILLED) {
              // 获取前个 then 的回调函数的返回值,传递给下个 then 
              // 若是返回一个普通值,直接调用 resolve 
              // 若返回的是一个 promise 对象,查看 promise 对象返回的结果,根据返回结果决定用 resolve 或 reject
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } else if (this.status === REJECTED) {
              let x = onRejected(this.reason); 
              resolvePromise(promise2, x, resolve, reject);
            } else {
              // 等待状态 
              // 将成功和失败回调先存储起来
              this.successCallback.push(onFulfilled); 
              this.failCallback.push(onRejected);
            }
          });
        }
      }
      

      完成 resolvePromise 函数

      function resolvePromise(promise2, x, resolve, reject) {
        // 判断 x 是不是 promise2
        // 循环引用报错 
        if(x === promise2) { 
          // reject报错 
          return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
        }
        
        // 如果是 Promise 实例直接调用 then 方法,免去了后面判断 thenable、取 then 方法的操作 
        if (x instanceof MyPromise) { 
          x.then(resolve, reject);
        }
        if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
          // 防止多次调用 
          let called;
          try {
            // A+ 规定,声明 then = x 的 then方法 
            let then = x.then;
            // 若 then 是函数,就默认是 promise 了
            if (typeof then === 'function') {
              then.call(x, y => {
                if (called) return;
                called = true;
                // resolve 的结果依旧是 promise 则继续解析
                resolvePromise(promise2, y, resolve, reject);
              }, err => { 
                // 成功和失败只能调用一个 
                if (called) return; 
                called = true; 
                reject(err);
              })
            } else {
              resolve(x);
            }
          } catch(e) {
            if (called) return;
            called = true; 
            // 取 then 出错了那就不要在继续执行了 
            reject(e);
          }
        } else {
          resolve(x);
        }
      }
      

      完善及最终完成版,包括 catchresolvereject 静态方法、finallyallrace

      // 创建三个常量用于表示状态,对于经常使用的一些值都应该通过常量来管理,便于开发及后期维护
      const PENDING = 'pending'; // 等待 
      const FULFILLED = 'fulfilled'; // 成功 
      const REJECTED = 'rejected'; // 失败
      
      class MyPromise {
        constructor(executor) {
          // 捕获执行器错误 
          // 若 executor 执行报错,则直接执行 reject 
          try {
            executor(this.resolve, this.reject);
          } catch (e) {
            this.reject(e);
          }
        }
      
        // 初始化 state 为等待状态
        state = PENDING; 
        // 成功的值 
        value = undefined; 
        // 失败的原因 
        reason = undefined; 
        // 成功回调存放 
        successCallback = []; 
        // 失败回调存放 
        failCallback = [];
      
        resolve = value => {
          // 若状态不是等待,则阻止程序向下执行 
          if (this.status !== PENDING) return;
          // 将状态改为成功
          this.status = FULFILLED; 
          // 保存成功后的值 
          this.value = value;
          // 成功回调是否存在,若存在则调用 
          // this.successCallback && this.successCallback(this.value); 
          
          // 从前到后执行回调
          // Array.shift() 取出数组第一个元素,然后()调用,shift 操作后,数组将删除该元素,直到数组为空
          while (this.successCallback.length) this.successCallback.shift()(this.value);
        }
      
        reject = reason => {
          // 若状态不是等待,则阻止程序向下执行
          if (this.status !== PENDING) return; 
          // 将状态改为成功 
          this.status = REJECTED;
          // 保存成功后的值 
          this.reason = reason; 
          // 成功回调是否存在,若存在则调用 
          // this.failCallback && this.failCallback(this.value) 
          
          // 从前到后执行回调 
          while (this.failCallback.length) this.failCallback.shift()(this.reason);
        }
      
        then(onFulfilled, onRejected) {
          // 参数可选
          onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value; 
          // 参数可选
          onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
        
          let promise2 = new MyPromise((resolve, reject) => {
            // 判断状态 
            // 下面 resolvePromise 传递的 promise2 其实是不能获取到 promise2,因为 promise2 是 new MyPromise 执行完成之后才有的 
            // 因此可以通过将下面处理代码变成异步代码,先让其他同步代码先执行完再去执行异步代码,就可以确保拿到 promise2
            const fulfilledHandle = () =>  {
              setTimeout(() => {
                try {
                  let x = onFulfilled(this.value);
                  resolvePromise(promise2, x, resolve, reject);
                } catch(e) {
                  // 此处的错误会在下个 then 的错误回调中捕获到
                  reject(e);
                }
              }, 0)
            };
            const rejectedHandle = () =>  {
              setTimeout(() => {
                try {
                  let x = onRejected(this.reason); 
                  resolvePromise(promise2, x, resolve, reject);
                } catch (e) {  
                  // 此处的错误会在下个 then 的错误回调中捕获到 
                  reject(e);
                }
              }, 0)
            };
          
            if (this.status === FULFILLED) {
              fulfilledHandle();
            } else if (this.status === REJECTED) {
              rejectedHandle();
            } else {
              this.successCallback.push(fulfilledHandle);
              this.failCallback.push(rejectedHandle);
            }
          })
        }
      }  
      
      // @param {*} promise2
      // @param {*} x    
      // @param {*} resolve  
      // @param {*} reject 
      function resolvePromise(promise2, x, resolve, reject) {  
        // 判断 x 是不是 promise2  
        // 循环引用报错 
        if (x === promise2) { 
          // reject报错 
          return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
        }
        
        // 若 x 是 Promise 实例则直接调用 then 方法,免去了后面判断 thenable、取 then 方法的操作
        if (x instanceof Promise) {
          return x.then(resolve, reject);
        }
        if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
          // 防止多次调用 
          let then, called;
          try {
            then = x.then;
          } catch (e) {
            // 一定要写 return,若访问 then 报错就不走后面判断 then 的逻辑了
            return reject(e);
          }
          if (typeof then === 'function') {
            try {
              then.call(x, y => {
                if (!called) {
                  called = true;
                  resolvePromise(promise, y, resolve, reject);
                }
              }, r => {
                if (!called) {
                  called = true;
                  reject(r);
                }
              })
            } catch (e) {
              if (!called) {
                reject(e);
              }
            }
          } else {
            resolve(x);
          }
        } else {
          resolve(x);
        }
      }
      
      // resolve 静态方法
      // 将给定的值包装成 promise 对象并返回
      class MyPromise {
        ...
        static resolve(value) {
          if (value instanceof MyPromise) return value;
          return new MyPromise((resolve, reject) => resolve(value));
        }
      }
      
      // reject 静态方法
      class MyPromise {
        ...
        static reject(reason) {
          return new MyPromise((resolve, reject) => {
             reject(reason);
           });
         }
       }
       
      // catch
      // 可以用来捕获当前 promise 对象的错误
      // 该方法不是一个静态方法,被定义在原型对象上
      class MyPromise {
        ...
        catch(onRejected) {
          return this.then(null, onRejected);
        }
      }
      
      // finally
      // 无论当前 Promise 对象最终是成功还是失败,finally 方法中的回调始终都会被执行一次
      // 在 finally 方法后可以链式调用 then 来拿到当前 Promise 对象最终返回的结果
      // 该方法不是一个静态方法,被定义在原型对象上
      class MyPromise {
        ...
        finally(callback) {
          // this.then 获取当前 promise 对象的状态
          return this.then(value => {
            return MyPromise.resolve(callback()).then(() => value);
          }, reason => {
            return MyPromise.resolve(callback()).then(() => { throw reason });
          })
        }
      }
      
      // all 方法
      // Promise.all 方法用来解决异步并发问题,允许按照异步代码调用的顺序得到异步代码执行的结果
      // 该方法的返回也是个 promise 对象,若所有任务都是成功的则返回的状态也是成功的,只要有一个失败则返回的状态是失败的
      class MyPromise {
        ...
        
        static all(array) {
          let result = [];
          let index = 0;
          return new MyPromise((resolve, reject) => {
            function addData(key, value) {
              result[key] = value;
              index ++;
              // 判断当所有任务都完成了才调用 resolve 去结束 all 方法 
              if(index === array.length) {
                resolve(result);
              }
            }
          
            for(let i = 0; i < array.length; i ++) {
              let current = array[i];
              if(current instanceof MyPromise) {
                // promise 对象
                current.then(value => addData(i, value), reason => reject(reason))
              } else {
                // 普通值
                addData(i, array[i]);
              }
            }
          });
        }
      }
      
      // race 方法
      // race 方法传入 Promise 对象数组
      // race 只返回执行最快的一个 Promise 结果,不论成功与失败
      // 返回结果与最快的 Promise 的成功与失败保持一致
      class MyPromise {
        ...
        static race(array) {
          return new MyPromise(function (resolve, reject) {
            for (let i = 0; i < array.length; i++) {
              let current = array[i];
              if (current instanceof MyPromise) {
                current.then(resolve, reject);
              } else {
                resolve(elem);
              }
            }
          }
        }
      }
      
      // deferred
      // 这个是 Promise 提供的一个快捷使用,自己实现 promise 时一定要加,否则 promises-aplus-tests promise.js 跑不过
      class MyPromise {
        ...
        
        deferred() {
          let result = {};
          dfd.promise = new MyPromise((resolve, reject) => {
            dfd.resolve = resolve;
            dfd.reject = reject;
          })
        }
      }  
      
  • 实例

    Promise 构造函数是同步执行的,promise.then 中的函数是异步执行的

    const promise = new Promise((resolve, reject) => { 
      console.log(1);
      resolve();
      console.log(2);
    });
    promise.then(() => {
      console.log(3);
    })
    console.log(4);
    // 1
    // 2
    // 4
    // 3
    

    promise 有 3 种状态:pending、fulfilled 或 rejected。状态改变只能是 pending -> fulfilled 或 pending -> rejected,状态一旦改变则不能再变。下面的 promise2 并不是 promise1,而是返回的一个新的 Promise 实例

    const promise1 = new   Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('success');
      }, 1000);
    });
    const promise2 = promise1.then(() => {
      throw new Error('error!!!');
    });
    console.log('promise1', promise1);
    console.log('promise2', promise2);
    
    setTimeout(() => {
      console.log('promise1', promise1);
      console.log('promise2', promise2);
    }, 2000)
    
    // promise1 Promise {<pending>}
    // promise2 Promise {<pending>}
    // Uncaught (in promise) Error: error!!! at <anonymous>:7:9
    // promise1 Promise {<fulfilled>: "success"}
    // promise2 Promise {<rejected>: Error: error!!! at <anonymous>:7:9
    

    构造函数中的 resolve 或 reject 只有第一次执行有效,多次调用没有任何作用,呼应结论:promise 状态一旦改变则不能再变

    const promise = new Promise((resolve, reject) => { 
      resolve('success1');
      reject('error');
      resolve('success2');
    });
    promise.then((res) => { 
      console.log('then: ', res);
    }).catch((err) => { 
      console.log('catch: ', err);
    })
    // then:  success1
    

    promise 可以链式调用

    • 提起链式调用通常会想到通过 return this 实现,不过 Promise 并不是这样实现的
    • promise 每次调用 then 或 catch 都会返回一个新的 promise,从而实现了链式调用
    Promise.resolve(1) .then((res) => { 
      console.log(res);
      return 2;
    }).catch((err) => {
      return 3;
    }).then((res) => {
      console.log(res);
    })
    // 1
    // 2
    

    promise 的 then 或 catch 可被调用多次,但这里 Promise 构造函数只执行一次,或者说 promise 内部状态一经改变且有了一个值,那么后续每次调用 then 或 catch 都会直接拿到该值

    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log('once');
        resolve('success');
      }, 1000)
    });
    const start = Date.now();
    promise.then((res) => { 
      console.log(res, Date.now() - start);
    });
    promise.then((res) => {
      console.log(res, Date.now() - start);
    });
    // once
    // success 1004
    // success 1004
    

    then 或 catch 中 return 一个 error 对象并不会抛出错误,因为返回任意一个非 promise 的值都会被包裹成 promise 对象,即 return new Error('error!!!') 等价于 return Promise.resolve(new Error('error!!!')),所以不会被后续的 catch 捕获

    Promise.resolve().then(() => { 
      return new Error('error!!!');
    }).then((res) => { 
      console.log('then: ', res);
    }).catch((err) => { 
      console.log('catch: ', err);
    })
    // then:  Error: error!!! at <anonymous>:2:10
    
    // 需要改成下面这样 catch 即可捕获到
    Promise.resolve().then(() => {
      return Promise.reject(new Error('error!!!'));
    }).then((res) => {
      console.log('then: ', res)
    }).catch((err) => {
      console.log('catch: ', err)
    })
    // catch:  Error: error!!! at <anonymous>:2:25
    

    then 或 catch 返回的值不能是 promise 本身,否则会造成死循环

    const promise = Promise.resolve().then(() => {
      return promise;
    });
    promise.catch(console.error);
    // TypeError: Chaining cycle detected for promise #<Promise>
    

    then 或 catch 的参数期望是函数,传入非函数则会发生值穿透

     Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log)
    // 1
    

    then 可以接收两个参数:处理成功的函数及处理错误的函数。catch 是 then 第二个参数的简便写法,但它们用法上有一点需要注意:then 的第二个处理错误的函数捕获不了第一个处理成功的函数抛出的错误,而后续的 catch 可以捕获之前的错误

    Promise.resolve().then(function success(res) {
      throw new Error('error');
    }, function fail1(e) {
      console.error('fail1: ', e);
    }).catch(function fail2(e) {
      console.error('fail2: ', e);
    })
    // fail2:  Error: error at success (<anonymous>:3:11)
    
    // 当然以下代码也可以
    Promise.resolve().then(function success1(res) {
      throw new Error('error');
    }, function fail1(e) { 
      console.error('fail1: ', e);
    }).then(function success2(res) {}, function fail2(e) { 
      console.error('fail2: ', e);
    })
    // fail2:  Error: error at success1 (<anonymous>:2:11)
    

    process.nextTick 和 promise.then 都属于 microtask(微任务),而 setImmediate 属于 macrotask(宏任务),在事件循环的 check 阶段执行。事件循环的每个阶段(macrotask)之间都会执行 microtask,事件循环的开始会先执行一次 microtask

    process.nextTick(() => { 
      console.log('nextTick');
    })
    Promise.resolve().then(() => {
      console.log('then');
    })
    setImmediate(() => { 
      console.log('setImmediate');
    })
    console.log('end');
    
    // end
    // nextTick
    // then
    // setImmediate
    

Generator

  • 简介

    GeneratorES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 是 ES6 中的一个新的语法,其最大的特点就是可以控制函数的执行

    语法上,首先可以把 Generator 函数理解为一个状态机,封装了多个内部状态

    执行 Generator 函数会返回一个遍历器对象,即 Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态

    形式上,Generator 函数是一个普通函数,但是有两个特征:

    • function 关键字与函数名之间有一个 *
    • 函数体内部使用 yield 表达式,定义不同的内部状态(yield 在英语里的意思就是产出
    function* helloGenerator() {
      yield 'hello';
      yield 'world';
      return 'generator';
    }
    
    var h = helloGenerator();
    

    上面代码定义了一个 Generator 函数 helloGenerator,它内部有yield表达式(helloworld),即该函数有三个状态:hello 、world 和 return 语句(结束执行)

    Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,即遍历器对象(Iterator Object)

    下一步,必须调用遍历器对象的 next 方法,使得指针移向下一个状态。即每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式或 return 语句为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而 next 方法可以恢复执行

    每次调用遍历器对象的 next 方法,会返回一个有着 valuedone 两个属性的对象

    • value 属性:表示当前的内部状态的值,是 yield 表达式后面那个表达式的值
    • done属性:是一个布尔值,表示是否遍历结束
    h.next(); // {value: 'hello', done: false}
    h.next(); // {value: 'world', done: false}
    h.next(); // {value: 'generator', done: true}
    h.next(); // {value: undefined, done: true}
    

    第一次调用 nextGenerator 函数开始执行,直到遇到第一个 yield 表达式为止。next 方法返回一个对象,它的 value 属性就是当前 yield 表达式的值 hellodone 属性的值 false,表示遍历还没有结束

    第二次调用 nextGenerator 函数从上次 yield 表达式停下的地方,一直执行到下一个 yield 表达式。next 方法返回的对象的 value 属性就是当前 yield 表达式的值 worlddone 属性的值 false,表示遍历还没有结束

    第三次调用 nextGenerator 函数从上次 yield 表达式停下的地方,一直执行到 return 语句(若没有 return 语句,就执行到函数结束)。next 方法返回的对象的 value 属性,就是紧跟在 return 语句后面的表达式的值(若没有 return 语句,则 value 属性的值为 undefined),done 属性的值 true,表示遍历已经结束

    第四次调用 next,此时 Generator 函数已经运行完毕,next 方法返回对象的 value 属性为 undefineddone 属性为 true,以后再调用 next 方法,返回的都是这个值

    ES6 没有规定 function 关键字与函数名之间的星号必须写在哪个位置,下面的写法都可以

    function * foo(x, y) { ··· }
    function *foo(x, y) { ··· }
    function* foo(x, y) { ··· } // 一般采用这种写法
    function*foo(x, y) { ··· }
    
  • yield 表达式

    由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数,yield 表达式就是暂停标志

    遍历器对象的 next 方法的运行逻辑如下:

    • 遇到 yield 表达式,就暂停执行后面的操作并将紧跟在 yield 后的那个表达式的值,作为返回的对象的 value 属性值

    • 下一次调用 next 方法时再继续往下执行,直到遇到下一个 yield 表达式

    • 若没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止并将 return 语句后的表达式的值,作为返回的对象的 value 属性值

    • 若该函数没有 return 语句,则返回的对象的 value 属性值为 undefined

    注意:yield 表达式后的表达式,只有当调用 next 方法、内部指针指向该语句时才会执行,因此可以理解为 JS 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能

    function* gen() {
      yield  123 + 456;
    }
    

    上面代码中,yield 后的表达式 123 + 456 不会立即求值,只会在 next 方法将指针移到这一句时才会求值

    yield 表达式与 return 语句既有相似之处,也有区别:

    • 相似之处:都能返回紧跟在语句后的那个表达式的值

    • 不同之处:每次遇到 yield,函数暂停执行,下一次再从该位置继续向后执行,而 return 语句不具备位置记忆的功能

      一个函数里面,只能执行一次或者说一个 return 语句,但可以执行多次或者说多个 yield 表达式

      正常函数只能返回一个值,因为只能执行一次 returnGenerator 函数可以返回一系列的值,因为可以有任意多个 yield

      从另一个角度看,Generator 生成了一系列的值,这也就是它的名称的来历(英语中 generator“生成器”的意思)

    Generator 函数可以不用 yield 表达式,这时就变成了一个单纯的暂缓执行函数

    function* gen() {
      console.log("执行了!");
    }
    let g1 = gen();
    console.log(g1); // gen {<suspended>}
    

    上面代码中,若函数 gen 是一个普通函数的话则调用时就会立即执行,打印"执行了!",但作为一个 Generator,调用时是不会立即执行的,只有在调用了 next 方法后才会执行

    function* gen() {
      console.log("执行了!");
    }
    let g1 = gen();
    console.log(g1.next()); 
    //  执行了!
    // {value: undefined, done: true}
    

    需要注意,yield 表达式只能用在 Generator 函数里,用在其他地方都会报错

    function f() {
      yield 123;
    }
    //  Uncaught SyntaxError: Unexpected number
    

    另外,yield 表达式若用在另一个表达式之中,必须放在圆括号里面

    function* demo() {
      console.log('Hello' + yield); // SyntaxError
      console.log('Hello' + yield 123); // SyntaxError
    
      console.log('Hello' + (yield)); // OK
      console.log('Hello' + (yield 123)); // OK
    }
    

    yield 表达式用作函数参数或放在赋值表达式的右边,可以不加括号

    function* demo() {
      foo(yield 'a', yield 'b'); // OK
      let input = yield; // OK
    }
    
  • 与 Iterator 接口的关系

    任意一个对象的 Symbol.iterator 方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象

    由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口

    var myIterable = {};
    myIterable[Symbol.iterator] = function* () {
      yield 1;
      yield 2;
      yield 3;
    };
    
    [...myIterable] // [1, 2, 3]
    

    上面代码中 Generator 函数赋值给 Symbol.iterator 属性,从而使得 myIterable 对象具有了 Iterator 接口,可以被 ... 运算符遍历了

    Generator 函数执行后返回一个遍历器对象,该对象本身也具有 Symbol.iterator 属性,执行后返回自身

    function* gen(){
      ...
    }
    var g = gen();
    g[Symbol.iterator]() === g; // true
    

    上面代码中 gen 是一个 Generator 函数,调用它会生成一个遍历器对象 g,它的 Symbol.iterator 属性也是一个遍历器对象生成函数,执行后返回它自己

  • next 方法的参数

    yield 表达式本身没有返回值,或者说总是返回 undefinednext 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值

    function* fn() {
      for(var i = 0; true; i++) {
        var reset = yield i;
        if(reset) { i = -1; }
      }
    }
    
    var g = fn();
    g.next(); // { value: 0, done: false }
    g.next(); // { value: 1, done: false }
    g.next(true); // { value: 0, done: false }
    

    上面代码先定义了一个可以无限运行的 Generator 函数 fnnext方法没有参数,每次运行到 yield 表达式,变量 reset 的值总是 undefined,当 next 方法带一个参数 true 时,变量 reset 就被重置为这个参数(即 true),因此 i 会等于 -1,下一轮循环就会从 -1 开始递增

    这个功能有很重要的语法意义,Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的,通过 next 方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值,即可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为

    再看一个例子

    function* foo(x) {
      let y = 2 * (yield (x + 1));
      let z = yield (y / 3);
      return (x + y + z);
    }
    
    let it = foo(5);
    it.next(); // {value: 6, done: false}
    it.next(); // {value: NaN, done: false}
    it.next(); // {value: NaN, done: true}
    
    let it = foo(5);
    it.next(); // {value: 6, done: false} 
    it.next(12); // {value: 8, done: false} 
    it.next(13); // {value: 42, done: true}
    

    上面代码中,不带参数时,第二次运行 next 方法时导致 y 的值等于 2 * undefined(即 NaN),除以 3 以后还是 NaN,因此返回对象的 value 属性也等于 NaN,第三次运行 next 方法时 z 等于 undefined,返回对象的 value 属性等于 5 + NaN + undefined,即 NaN

    若向 next 方法提供参数,返回结果则完全不一样。上面代码第一次调用 next 方法时,返回 x+1 的值 6;第二次调用 next 方法,将上一次 yield 表达式的值设为 12,因此 y 等于 24,返回 y / 3 的值 8;第三次调用 next 方法,将上一次 yield 表达式的值设为 13,因此 z 等于 13,这时 x 等于 5y 等于 24,所以 return 语句的值等于 42

    注意,由于 next 方法的参数表示上一个 yield 表达式的返回值,所以在第一次使用 next 方法时,传递参数是无效的。V8 引擎直接忽略第一次使用 next 方法时的参数,只有从第二次使用 next 方法开始,参数才是有效的。从语义上讲,第一个 next 方法用来启动遍历器对象,所以不用带有参数

    再看一个通过 next 方法的参数,向 Generator 函数内部输入值的例子

    function* dataConsumer() {
      console.log('Started');
      console.log(`1. ${yield}`);
      console.log(`2. ${yield}`);
      return 'result';
    }
    let genObj = dataConsumer();
    genObj.next(); // Started
    genObj.next('a'); // 1. a
    genObj.next('b'); // 2. b
    

    若想要第一次调用 next 方法时就能够输入值,可以在 Generator 函数外面再包一层

    function wrapper(generatorFn) {
      return function (...args) {
        let genObj = generatorFn(...args);
        genObj.next();
        return genObj;
      };
    }
    
    const wrapped = wrapper(function* () {
      console.log(`Input: ${yield}`);
      return 'DONE';
    });
    
    wrapped().next('hi!')
    // Input: hi!
    // {value: 'DONE', done: true}
    
  • for...of 循环

    for...of 循环可以自动遍历 Generator 函数运行时生成的 Iterator 对象,且此时不再需要调用 next 方法

    function* foo() {
      yield 1;
      yield 2;
      yield 3;
      yield 4;
      yield 5;
      return 6;
    }
    for (let v of foo()) {
      console.log(v);
    }
    // 1 2 3 4 5
    

    上面代码使用 for...of 循环,依次显示 5 个 yield 表达式的值,这里需要注意,一旦 next 方法的返回对象的 done 属性为truefor...of 循环就会中止且不包含该返回对象,所以上面代码的 return 语句返回的 6 不包括在 for...of 循环之中

    下面是一个利用 Generator 函数和 for...of 循环,实现斐波那契数列的例子

    function* fibonacci() {
      let [prev, curr] = [0, 1];
      for (;;) {
        yield curr;
        [prev, curr] = [curr, prev + curr];
      }
    }
    
    for (let n of fibonacci()) {
      if (n > 10) break;
      console.log(n);
    }
    // 1 1 2 3 5 8
    

    从上面代码可见,使用 for...of 语句时不需要使用 next 方法

    利用 for...of 循环可以写出遍历任意对象的方法。原生的 JS 对象没有遍历接口,无法使用 for...of 循环,通过 Generator 函数为它加上这个接口,就可以用了

    function* objEntries(obj) {
      let propKeys = Reflect.ownKeys(obj);
      for (let propKey of propKeys) {
        yield [propKey, obj[propKey]];
      }
    }
    
    let name = { first: 'tn', last: 'tnna' };
    
    for (let [key, value] of objEntries(name)) {
      console.log(`${key}: ${value}`);
    }
    // first: tn
    // last: tnna
    

    上面代码中,对象 name 原生不具备 Iterator 接口,无法用 for...of 遍历,这时我们可以通过 Generator 函数 objEntries 为它加上遍历器接口,就可以使用 for...of 遍历了

    加上遍历器接口的另一种写法是,将 Generator 函数加到对象的 Symbol.iterator 属性上面

    function* objEntries() {
      let propKeys = Object.keys(this);
      for (let propKey of propKeys) {
        yield [propKey, this[propKey]];
      }
    }
    
    let name = { first: 'tn', last: 'tnna' };
    name[Symbol.iterator] = objEntries;
    
    for (let [key, value] of name) {
      console.log(`${key}: ${value}`);
    }
    // first: tn
    // last: tnna
    

    除了 for...of 循环以外,扩展运算符(...)解构赋值Array.from 方法内部调用的也都是遍历器接口,这意味着它们均可将 Generator 函数返回的 Iterator 对象作为参数

    function* numbers () {
      yield 1;
      yield 2;
      return 3;
      yield 4;
    }
    
    // 扩展运算符
    [...numbers()]; // [1, 2]
    
    // Array.from 方法
    Array.from(numbers()); // [1, 2]
    
    // 解构赋值
    let [x, y] = numbers();
    x // 1
    y // 2
    
    // for...of 循环
    for (let n of numbers()) {
      console.log(n);
    }
    // 1
    // 2
    
  • yield* 表达式

    若在 Generator 函数内部调用另一个 Generator 函数,需要在前者的函数体内部,自己手动完成遍历

    function* foo() {
      yield 'a';
      yield 'b';
    }
    
    function* bar() {
      yield 'x';
      // 手动遍历 foo()
      for (let i of foo()) {
        console.log(i);
      }
      yield 'y';
    }
    
    for (let v of bar()){
      console.log(v);
    }
    // x
    // a
    // b
    // y
    

    上面代码中 foobar 均是 Generator 函数,在 bar 里面调用 foo 就需要手动遍历 foo,若有多个 Generator 函数嵌套,写起来就非常麻烦

    ES6 提供 yield* 表达式作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数

    function* bar() {
      yield 'x';
      yield* foo();
      yield 'y';
    }
    
    // 等同于
    function* bar() {
      yield 'x';
      yield 'a';
      yield 'b';
      yield 'y';
    }
    
    // 等同于
    function* bar() {
      yield 'x';
      for (let v of foo()) {
        yield v;
      }
      yield 'y';
    }
    
    for (let v of bar()){
      console.log(v);
    }
    // x
    // a
    // b
    // y
    

    再来看一个对比的例子

    function* inner() {
      yield 'hello!';
    }
    
    function* outer1() {
      yield 'open';
      yield inner();
      yield 'close';
    }
    
    var gen = outer1();
    gen.next(); // {value: 'open', done: false}
    gen.next(); // {value: inner, done: false}
    gen.next(); // {value: 'close', done: false}
    
    function* outer2() {
      yield 'open'
      yield* inner()
      yield 'close'
    }
    var gen = outer2();
    gen.next(); // {value: 'open', done: false}
    gen.next(); // {value: 'hello!', done: false}
    gen.next(); // {value: 'close', done: false}
    

    上面例子中,outer2 使用了 yield*outer1 没使用,结果就是 outer1 返回一个遍历器对象,outer2 返回该遍历器对象的内部值

    从语法角度看,若 yield 表达式后面跟的是一个遍历器对象,需要在 yield 表达式后面加上星号,表明它返回的是一个遍历器对象,这被称为 yield* 表达式

    yield* 后面的 Generator 函数没有 return 语句时等同于在 Generator 函数内部部署一个 for...of 循环

    function* test(iter1, iter2) {
      yield* iter1;
      yield* iter2;
    }
    
    // 等同于
    function* test(iter1, iter2) {
      for (var value of iter1) {
        yield value;
      }
      for (var value of iter2) {
        yield value;
      }
    }
    

    上面代码中 yield* 后面的 Generator 函数没有 return 语句时,是 for...of 的一种简写形式,完全可以用后者替代前者

    若有 return 语句时则需要用 var value = yield* iterator 的形式获取 return 语句的值

    yield* 后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员

    function* gen() {
      yield* ["a", "b", "c"];
    }
    
    gen().next(); // {value: 'a', done: false}
    

    实际上任何数据结构只要有 Iterator 接口就可以被 yield* 遍历

    const read = (function* () {
      yield 'hello';
      yield* 'hello';
    })();
    read.next(); // {value: 'hello', done: false}
    read.next(); // {value: 'h', done: false}
    

    上面代码中 yield 表达式返回整个字符串,yield* 语句返回单个字符,因为字符串具有 Iterator 接口,所以被 yield* 遍历

    若被代理的 Generator 函数有 return 语句,则就可向代理它的 Generator 函数返回数据

    function* foo() {
      yield 2;
      yield 3;
      return "foo";
    }
    
    function* bar() {
      yield 1;
      var v = yield* foo();
      console.log("v: " + v);
      yield 4;
    }
    var it = bar();
    it.next(); // {value: 1, done: false}
    it.next(); // {value: 2, done: false}
    it.next(); // {value: 3, done: false}
    it.next(); 
    // v: foo
    // {value: 4, done: false}
    it.next(); // {value: undefined, done: true}
    

    上面代码在第四次调用 next 方法时会有输出,这是因为函数 fooreturn 语句向函数 bar 提供了返回值

    再看一个例子

    function* genFuncWithReturn() {
      yield 'a';
      yield 'b';
      return 'c';
    }
    function* logReturned(genObj) {
      let result = yield* genObj;
      console.log(result);
    }
    
    [...logReturned(genFuncWithReturn())]
    // c
    // ['a', 'b']
    

    上面代码中存在两次遍历:第一次是 ... 扩展运算符遍历函数 logReturned 返回的遍历器对象,第二次是 yield* 语句遍历函数 genFuncWithReturn 返回的遍历器对象

    这两次遍历的效果是叠加的,最终表现为 ... 扩展运算符遍历函数 genFuncWithReturn 返回的遍历器对象,所以最后的数据表达式得到的值等于 [ 'a', 'b' ]。但函数 genFuncWithReturnreturn 语句返回值 c 会返回给函数 logReturned 内部的 result 变量,因此会有终端输出

    yield* 命令可以很方便地取出嵌套数组的所有成员

    function* iterTree(tree) {
      if (Array.isArray(tree)) {
        for(let i = 0; i < tree.length; i++) {
          yield* iterTree(tree[i]);
        }
      } else {
        yield tree;
      }
    }
    const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
    for(let x of iterTree(tree)) {
      console.log(x);
    }
    // a b c d e
    
    // 由于扩展运算符 `...` 默认调用 `Iterator` 接口,所以上面这个函数也可以用于嵌套数组的平铺
    [...iterTree(tree)]; // ["a", "b", "c", "d", "e"]
    
  • 作为对象属性的 Generator 函数

    若一个对象的属性是 Generator 函数,可以简写成下面的形式

    const obj = {
      * genMethod() {
        // ···
      }
    };
    
    // 等价于
    const obj = {
      genMethod: function* () {
        // ···
      }
    };
    

    上面代码中 genMethod 属性前面有一个星号,表示这个属性是一个 Generator 函数

  • Generator 函数的 this

    Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的 prototype 对象上的方法

    function* gen() {}
    gen.prototype.hello = function () {
      return 'say hi!';
    };
    
    const obj = gen();
    obj instanceof gen; // true
    obj.hello(); // 'say hi!'
    

    上面代码中,Generator 函数 gen 返回的遍历器 obj,是 gen 的实例,而且继承了 gen.prototype。但若把 gen 当作普通的构造函数并不会生效,因为 gen 返回的总是遍历器对象,而不是 this 对象

    function* gen() {
      this.a = 11;
    }
    
    let obj = gen();
    obj.next();
    obj.a; // undefined
    

    上面代码中 Generator 函数 genthis 对象上面添加了一个属性 a,但是 obj 对象拿不到这个属性

    Generator 函数也不能跟 new 命令一起用,会报错

    function* F() {
      yield this.x = 2;
      yield this.y = 3;
    }
    
    new F(); // Uncaught TypeError: F is not a constructor
    

    有办法让 Generator 函数返回一个正常的对象实例,既可以用 next 方法又可获得正常的 this 吗?

    下面是一个变通方法。首先生成一个空对象,使用 call 方法绑定 Generator 函数内部的 this,这样构造函数调用后该空对象就是 Generator 函数的实例对象了

    function* F() {
      this.a = 1;
      yield this.b = 2;
      yield this.c = 3;
    }
    var obj = {};
    var f = F.call(obj);
    f.next(); // {value: 2, done: false}
    f.next(); // {value: 3, done: false}
    f.next(); // {value: undefined, done: true}
    
    obj.a; // 1
    obj.b; // 2
    obj.c; // 3
    

    上面代码中,首先是 F 内部的 this 对象绑定 obj 对象然后调用它,返回一个 Iterator 对象,这个对象执行三次 next 方法(因为 F 内部有两个 yield 表达式),完成 F 内部所有代码的运行。此时所有内部属性都绑定在 obj 对象上了,因此 obj 对象也就成了 F 的实例

    上面代码中,执行的是遍历器对象 f,但是生成的对象实例是 obj,有没有办法将这两个对象统一呢?一个办法就是将 obj 换成F.prototype

    function* gen() {
      this.a = 1;
      yield this.b = 2;
      yield this.c = 3;
    }
    
    var f = F.call(F.prototype);
    
    // 将 F 改成构造函数,就可以对它执行 new 命令
    function F() {
      return gen.call(gen.prototype);
    }
    
    var f = new F();
    f.next();  // {value: 2, done: false}
    f.next();  // {value: 3, done: false}
    f.next();  // {value: undefined, done: true}
    
    f.a; // 1
    f.b; // 2
    f.c; // 3
    
  • 应用

    Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景

    • 异步操作的同步化表达

      Generator 函数的暂停执行的效果,意味着可以把异步操作写在 yield 表达式里,等到调用 next 方法时再往后执行,这实际上等同于不需要写回调函数,因为异步操作的后续操作可以放在 yield 表达式下面,反正要等到调用 next 方法时再执行。所以 Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数

      Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达

      function* main() {
        var result = yield request("http://some.url");
        var resp = JSON.parse(result);
        console.log(resp.value);
      }
      
      function request(url) {
        makeAjaxCall(url, function(response){
          it.next(response);
        });
      }
      
      var it = main();
      it.next();
      

      上面代码的 main 函数就是通过 Ajax 操作获取数据。可以看到除了多了一个 yield,它几乎与同步操作的写法完全一样。注意 makeAjaxCall 函数中的 next 方法,必须加上 response 参数,因为 yield 表达式本身是没有值的,总是等于undefined

      下面另一个例子是通过 Generator 函数逐行读取文本文件

      function* numbers() {
        let file = new FileReader("numbers.txt");
        try {
          while(!file.eof) {
            yield parseInt(file.readLine(), 10);
          }
        } finally {
          file.close();
        }
      }
      
    • 控制流管理

      若有一个多步操作非常耗时,采用回调函数,可能会写成下面这样

      step1(function (value1) {
        step2(value1, function(value2) {
          step3(value2, function(value3) {
            step4(value3, function(value4) {
              ...
            });
          });
        });
      });
      

      采用 Promise 改写上面的代码

      Promise.resolve(step1)
       .then(step2)
       .then(step3)
       .then(step4)
       .then(function (value4) {
         ...
       }, function (error) {
         ...
       })
       .done();
      

      上面代码已经把回调函数改成了直线执行的形式,加入了大量 Promise 的语法,Generator 函数可以进一步改善代码运行流程

      function* longRunningTask(value1) {
        try {
          var value2 = yield step1(value1);
          var value3 = yield step2(value2);
          var value4 = yield step3(value3);
          var value5 = yield step4(value4);
          // Do something with value4
        } catch (e) {
          // Handle any error from step1 through step4
        }
      }
      

      然后使用一个函数,按次序自动执行所有步骤

      scheduler(longRunningTask(initialValue));
      
      function scheduler(task) {
        var taskObj = task.next(task.value);
        // 若 Generator 函数未结束,就继续调用
        if (!taskObj.done) {
          task.value = taskObj.value
          scheduler(task);
        }
      }
      

      注意,上面这种做法只适合同步操作,即所有的 task 都必须是同步的,不能有异步操作。因为这里的代码一得到返回值就继续往下执行,没有判断异步操作何时完成

      下面利用 for...of 循环会自动依次执行 yield 命令的特性,提供一种更一般的控制流管理的方法

      let steps = [step1Fun, step2Fun, step3Fun];
      function* iterateSteps(steps) {
        for (let i = 0; i < steps.length; i++) {
          let step = steps[i];
          yield step();
        }
      }
      

      上面代码中数组 steps 封装了一个任务的多个步骤,Generator 函数 iterateSteps 则是依次为这些步骤加上 yield 命令

      将任务分解成步骤之后还可以将项目分解成多个依次执行的任务

      let jobs = [job1, job2, job3];
      function* iterateJobs(jobs) {
        for (var i = 0; i < jobs.length; i++) {
          var job = jobs[i];
          yield* iterateSteps(job.steps);
        }
      }
      

      上面代码中数组 jobs 封装了一个项目的多个任务,Generator 函数 iterateJobs 则是依次为这些任务加上 yield* 命令

      最后就可以用 for...of 循环一次性依次执行所有任务的所有步骤

      for (var step of iterateJobs(jobs)){
        console.log(step.id);
      }
      

      上面的做法只能用于所有步骤都是同步操作的情况,不能有异步操作的步骤。for...of 的本质是一个 while 循环,所以上面的代码实质上执行的是下面的逻辑

      var it = iterateJobs(jobs);
      var res = it.next();
      
      while (!res.done){
        var result = res.value;
        // ...
        res = it.next();
      }
      
    • 部署 Iterator 接口

      利用 Generator 函数,可以在任意对象上部署 Iterator 接口

      function* iterEntries(obj) {
        let keys = Object.keys(obj);
        for (let i=0; i < keys.length; i++) {
          let key = keys[i];
          yield [key, obj[key]];
        }
      }
      
      let myObj = { foo: 3, bar: 7 };
      
      for (let [key, value] of iterEntries(myObj)) {
        console.log(key, value);
      }
      // foo 3
      // bar 7
      

      上述代码中 myObj 是一个普通对象,通过 iterEntries 函数就有了 Iterator 接口,即可以在任意对象上部署 next 方法

      下面是一个对数组部署 Iterator 接口的例子,尽管数组原生具有这个接口

      function* makeSimpleGenerator(arr){
        var nextIndex = 0;
        while(nextIndex < arr.length){
          yield arr[nextIndex ++];
        }
      }
      
      var gen = makeSimpleGenerator(['yo', 'ya']);
      gen.next(); // {value: 'yo', done: false}
      gen.next(); // {value: 'ya', done: false}
      gen.next(); // {value: undefined, done: true}
      
    • 作为数据结构

      Generator 可以看作是数据结构,更确切地说可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式提供类似 数组 的接口

      function* doStuff() {
        yield fs.readFile.bind(null, 'hello.txt');
        yield fs.readFile.bind(null, 'world.txt');
        yield fs.readFile.bind(null, 'and-such.txt');
      }
      

      上面代码就是依次返回三个函数,但是由于使用了 Generator 函数,导致可以像处理数组那样处理这三个返回的函数

      for (task of doStuff()) {
        // task 是一个函数,可以像回调函数那样使用它
      }
      

      实际上,若用 ES5 表达,完全可以用数组模拟 Generator 的这种用法

      function doStuff() {
        return [
          fs.readFile.bind(null, 'hello.txt'),
          fs.readFile.bind(null, 'world.txt'),
          fs.readFile.bind(null, 'and-such.txt')
        ];
      }
      

      上面的函数可以用一模一样的 for...of 循环处理,两相一比较就不难看出 Generator 使得数据或操作具备了类似数组的接口

async / await

  • 背景

    在 JS 中处理异步操作的回调 (callback) 通常会导致多嵌套的代码块,俗称回调地狱,这样的代码复杂,可读性、可维护性非常不友好

    ES6 Promise 的出现,使得能够扁平化回调函数,告别回调地狱,写出优雅的代码。但是在实践中发现 Promise 并不完美,若 Promise 的回调中出现嵌套,依旧会出现回调地狱

    async/await 的出现提供了一种新的编写异步代码方式,使得异步代码看起来像是同步代码

  • 含义

    ES2017 标准引入了 async 函数,使得异步操作变得更加方便,async 的本质就是 Generator 函数的语法糖

    下面 Generator 函数,依次读取两个文件

    const fs = require('fs');
    const readFile = function (fileName) {
      return new Promise(function (resolve, reject) {
        fs.readFile(fileName, function(error, data) {
          if (error) return reject(error);
          resolve(data);
        });
      });
    };
    
    const gen = function* () {
      const f1 = yield readFile('/etc/fstab');
      const f2 = yield readFile('/etc/shells');
      console.log(f1.toString());
      console.log(f2.toString());
    };
    
    // gen 可以写成 async 函数
    const asyncReadFile = async function () {
      const f1 = await readFile('/etc/fstab');
      const f2 = await readFile('/etc/shells');
      console.log(f1.toString());
      console.log(f2.toString());
    };
    

    一比较就会发现 async 函数看起来就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已

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

    • 内置执行器Generator 函数的执行必须靠执行器,所以才有了如 co 模块,而 async 函数自带执行器,因此 async 函数的执行与普通函数一模一样,只要一行

      asyncReadFile();
      ``
      调用 `asyncReadFile` 函数,然后它就会自动执行,输出最后结果。这完全不像 `Generator` 函数需要调用 `next` 方法或用 `co` 模块才能真正执行得到最后结果
      
      
    • 更好的语义async/await 比起 *yield来说语义更清晰,async 表示函数里有异步操作,await 表示紧跟在后面的表达式需等待结果

    • 更广的适用性async 函数的 await 命令后面可以是 Promise 对象、原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolvedPromise 对象);而 co 模块约定 yield 命令后面只能是 Thunk 函数或 Promise 对象

    • 返回值是 Promiseasync 函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了,可以用 then 方法指定下一步的操作

    进一步说,async 函数完全可以看作多个异步操作包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖

  • 基本用法

    async 函数有多种使用形式

    // 函数声明
    async function foo() {}
    
    // 函数表达式
    const foo = async function() {};
    
    // 对象的方法
    let obj = { async foo() {} };
    obj.foo().then(...)
    
    // 箭头函数
    const foo = async () => {};
    
    // Class 的方法
    class Storage {
      constructor() {
        this.cachePromise = caches.open('avatars');
      }
    
      async getAvatar(name) {
        const cache = await this.cachePromise;
        return cache.match(`/avatars/${name}.jpg`);
      }
    }
    
    const storage = new Storage();
    storage.getAvatar('jake').then(...);
    

    async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行时一旦遇到 await 就会先返回,等到异步操作完成再接着执行函数体内后面的语句

    // 指定 50 毫秒以后输出 `hello world`
    function timeout(ms) {
      return new Promise((resolve) => {
        setTimeout(resolve, ms);
      });
    }
    
    async function asyncPrint(value, ms) {
      await timeout(ms);
      console.log(value);
    }
    
    asyncPrint('hello world', 50);
    

    async 关键字表明该函数内部有异步操作,调用该函数时会立即返回一个 Promise 对象,由于 async 函数返回的是 Promise 对象,可以作为 await 命令的参数,所以上面的例子也可以写成下面的形式

    async function timeout(ms) {
      await new Promise((resolve) => {
        setTimeout(resolve, ms);
      });
    }
    
    async function asyncPrint(value, ms) {
      await timeout(ms);
      console.log(value);
    }
    
    asyncPrint('hello world', 50);
    
  • 语法规则

    async 函数返回一个 Promise 对象,async 函数内部 return 语句返回的值会成为 then 方法回调函数的参数

    async function fn() {
      return 'hello world!';
    }
    
    fn().then(value => console.log(value)); // hello world!
    

    async 函数内部抛出错误时会导致返回的 Promise 对象变为 rejected 状态,抛出的错误对象会被 catch 方法回调函数接收到

    async function fn() {
      throw new Error('wrong!!!');
    }
    
    fn().then(value => console.log('resolve', value), reason => console.log('reject', reason))
    // reject Error: wrong!!!
    

    async 函数返回的 Promise 对象必须等到内部所有 await 命令后的 Promise 对象执行完后才会发生状态改变,除非遇到 return 语句或抛出错误,即只有 async 函数内部的异步操作执行完才会执行 then 方法指定的回调函数

    async function getTitle(url) {
      let response = await fetch(url);
      let html = await response.text();
      return html.match(/<title>([\s\S]+)</title>/i)[1];
    }
    getTitle('https://tc39.github.io/ecma262/').then(console.log);
    // "ECMAScript 2017 Language Specification"
    

    函数 getTitle 内部有三个操作:抓取网页、取出文本、匹配页面标题,只有这三个操作全部完成才会执行 then 方法里面的 console.log

    await 命令:

    • 正常情况下 await 命令后面是一个 Promise 对象,返回该对象的结果若不是 Promise 对象就直会调用 Promise.resolve() 进行转化再返回

      async function fn() {
        // 等同于
        // return 123;
        return await 123;
      }
      
      fn().then(value => console.log(value)); // 123
      
    • 另一种情况,await 命令后面是一个 thenable 对象(即定义了 then 方法的对象),则 await 会将其等同于 Promise 对象

      class Sleep {
        constructor(timeout) {
          this.timeout = timeout;
        }
        then(resolve, reject) {
          const startTime = Date.now();
          setTimeout(() => resolve(Date.now() - startTime), this.timeout);
        }
      }
      
      (async () => {
        const sleepTime = await new Sleep(1000);
        console.log(sleepTime);
      })();
      // 1000
      

      上面 await 命令后面是一个 Sleep 对象的实例,这个实例不是 Promise 对象,但是因为定义了 then 方法,await 会将其视为 Promise 处理

    JS 一直没有休眠的语法,但借助 await 命令就可以让程序停顿指定的时间

    function sleep(interval) {
      return new Promise(resolve => {
        setTimeout(resolve, interval);
      })
    }
    
    // 用法
    async function oneTimeWait() {
      for(let i = 1; i <= 5; i++) {
        console.log(i);
        await sleep(1000);
      }
    }
    oneTimeWait();
    

    await 命令后面的 Promise 对象若变为 rejected 状态,则 reject 的参数会被 catch 方法的回调函数接收到

    async function fn() {
      await Promise.reject('wrong!!!');
    }
    
    fn().then(value => console.log(value)).catch(reason => console.log(reason));
    // wrong!!!
    

    注意,上面代码中 await 语句前面没有 return,但是 reject 方法的参数依然传入了 catch 方法的回调函数,这里若在 await 前面加上 return,效果是一样的

    任何一个 await 语句后面的 Promise 对象变为 reject 状态,则整个 async 函数都会中断执行

    async function fn() {
      await Promise.reject('wrong!!!');
      await Promise.resolve('hello world'); // 不会执行
    }
    

    有时我们希望即使前一个异步操作失败也不要中断后面的异步操作,这时可以将第一个 await 放在 try...catch 结构里,这样不管这个异步操作是否成功,第二个 await 都会执行

    async function fn() {
      try {
        await Promise.reject('wrong!!!');
      } catch(e) {}
      return await Promise.resolve('hello world!');
    }
    
    fn().then(value => console.log(value)); // hello world!
    

    另一种方法是 await 后面的 Promise 对象跟一个 catch 方法,处理前面可能出现的错误

    async function fn() {
      await Promise.reject('wrong!!!').catch(e => console.log(e));
      return await Promise.resolve('hello world!');
    }
    
    fn().then(value => console.log(value));
    // wrong!!!
    // hello world!
    

    使用注意点:

    • 上面已经说过 await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中

    • 多个 await 命令后面的异步操作,若不存在继发关系,最好让它们同时触发。以下两种写法 getFoogetBar 都是同时触发,这样就会缩短程序的执行时间

      // 写法一
      let [foo, bar] = await Promise.all([getFoo(), getBar()]);
      
      // 写法二
      let fooPromise = getFoo();
      let barPromise = getBar();
      let foo = await fooPromise;
      let bar = await barPromise;
      
    • await 命令只能用在 async 函数中,若用在普通函数就会报错

      async function fn(db) {
        let arr = [{}, {}, {}];
        arr.forEach(function (item) {
          await db.post(item);
        });
      }
      // Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules
      

      此处注意:若将 forEach 方法的参数改成 async 函数,也可能有问题。如下面代码,可能不会正常工作,原因是这时三个 db.post() 操作将是并发执行,而不是继发执行,正确的写法是采用 for 循环

      function fn(db) { 
        let arr = [{}, {}, {}];
        arr.forEach(async function (item) {
          await db.post(item);
        });
      }
      

      另一种方法是使用数组的 reduce() 方法

      async function fn(db) {
        let arr = [{}, {}, {}];
      
        await arr.reduce(async (_, item) => {
          await _;
          await db.post(item);
        }, undefined);
      }
      

      上面例子中,reduce() 方法的第一个参数是 async 函数,导致该函数的第一个参数是前一步操作返回的 Promise 对象,所以必须使用 await 等待它操作结束。另外,reduce() 方法返回的是 arr 数组最后一个成员的 async 函数的执行结果,也是一个 Promise 对象,在它前面也必须加上 await

      上面的 reduce() 的参数函数里没有 return 语句,原因是这个函数的主要目的是 db.post() 操作,不是返回值。而且 async 函数不管有没有 return 语句,总是返回一个 Promise 对象,所以这里的 return 是不必要的

      若确实希望多个请求并发执行,可以使用 Promise.all 方法,当三个请求都会 resolved 时,下面两种写法效果相同

      async function fn(db) {
        let arr = [{}, {}, {}];
        let promises = arr.map((item) => db.post(item));
      
        let results = await Promise.all(promises);
        console.log(results);
      }
      
      // 或者使用下面的写法
      async function fn(db) {
        let arr = [{}, {}, {}];
        let promises = arr.map((item) => db.post(item));
      
        let results = [];
        for (let promise of promises) {
          results.push(await promise);
        }
        console.log(results);
      }
      
  • async 函数的实现原理

    async 函数的实现原理就是将 Generator 函数和自动执行器包装在一个函数里

    async function fn(args) {...}
    
    // 等同于
    function fn(args) {
      return spawn(function* () {...});
    }
    

    所有的 async 函数都可以写成上面的第二种形式,其中的 spawn 函数就是自动执行器

    下面给出 spawn 函数的实现

    function spawn(gen) {
      return new Promise(function(resolve, reject) {
        const g = gen();
        function step(nextF) {
          let next;
          try {
            next = nextF();
          } catch(e) {
            return reject(e);
          }
          if(next.done) {
            return resolve(next.value);
          }
          Promise.resolve(next.value).then(function(v) {
            step(function() { return g.next(v); });
          }, function(e) {
            step(function() { return g.throw(e); });
          });
        }
        step(function() { return g.next(undefined); });
      });
    }
    
  • 与其他异步处理方法的比较

    通过 async/await 可以彻底摆脱回调地狱,以同步方式编写异步代码,代码简洁,十分友好

    通过一个例子来看 async 函数与 PromiseGenerator 函数的比较

    假定某个 DOM 元素上面部署了一系列的动画,前一个动画结束才能开始后一个,若当中有一个动画出错就不再往下执行,返回上一个成功执行的动画的返回值

    Promise 的写法

    function chainAnimationsPromise(elem, animations) {
      // 变量 ret 用来保存上一个动画的返回值
      let ret = null;
    
      // 新建一个空的 Promise
      let p = Promise.resolve();
    
      // 使用 then 方法添加所有动画
      for(let anim of animations) {
        p = p.then(function(val) {
          ret = val;
          return anim(elem);
        });
      }
    
      // 返回一个部署了错误捕捉机制的 Promise
      return p.catch(function(e) {
        /* 忽略错误,继续执行 */
      }).then(function() {
        return ret;
      });
    }
    

    虽然 Promise 的写法比回调函数的写法大大改进,但是一眼看上去代码完全都是 PromiseAPIthencatch等),操作本身的语义反而不容易看出来

    Generator 函数的写法

    function chainAnimationsGenerator(elem, animations) {
      return spawn(function*() {
        let ret = null;
        try {
          for(let anim of animations) {
            ret = yield anim(elem);
          }
        } catch(e) {
          /* 忽略错误,继续执行 */
        }
        return ret;
      });
    }
    

    上面代码使用 Generator 函数遍历了每个动画,语义比 Promise 写法更清晰,用户定义的操作全部都出现在 spawn 函数的内部。这个写法的问题在于必须有一个任务运行器来自动执行 Generator 函数,上面的 spawn 函数就是自动执行器,它返回一个 Promise 对象,而且必须保证 yield 语句后面的表达式返回一个 Promise

    async 函数的写法

    async function chainAnimationsAsync(elem, animations) {
      let ret = null;
      try {
        for(let anim of animations) {
          ret = await anim(elem);
        }
      } catch(e) {
        /* 忽略错误,继续执行 */
      }
      return ret;
    }
    

    可以看到 async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将 Generator 写法中的自动执行器改在语言层面提供,不暴露给用户,因此代码量最少

    错误处理try...catch 可以处理同步、异步错误。如下代码块 一 中,try...catch 并不能捕捉 then 方法中 JSON.parse 异常出错,因为它在 Promise 中,我们需要使用 catch,这样错误处理代码非常冗余且在实际生产代码会更加复杂;但是在代码块 二 中,使用了 async/await 使得 try...catch 能捕捉 JSON.parse 可能的异常出错

    // 代码块一
    function test () {
      try {
        getJSON().then((res) => {
          var data = JSON.parse(res); // 此处可能出错 
        })
      } catch (e) { ... } 
    }
    
    // 代码块二 
    async function test () {
      try {
        var data = JSON.parse(await getJSON());
      } catch (e) { ... }
    }
    

    中间值:有时会遇到这样的情形,promise1 返回值 value1,promise2 依赖 value1,返回value2,promise3 依赖 value1 和 value2。最简单的做法是通过嵌套解决 promise 间的依赖,显然这种简单粗暴的处理方式使得又掉进回调地狱了,还可以通过中间变量来抹平这个回调嵌套,而通过 async/await 可以最优、最简洁的解决这个问题。注意,在下面代码的返回语句 promise3 并没有 await,因为异步函数的会将其返回值隐式封装在 Promise.resolve 中

    function test() {
      return promise1().then(value1 => {
        return promise2(valu1).then(value2 => {
          return promise3(value1, value2);
        })
      })
    } 
    
    function test() {
      var value1;
      var value2;
      return promise1().then(vle => {
        value1 = vle;
        return promise2(value1); 
      }).then(vle => {
        value2 = vle;
        return promise3(value1, value2);
      })
    }
    
    async function test() {
      var value1 = await promise1();
      var value2 = await promise2(value1);
      return promise3(value1, value2);
    }
    

    条件语句:使用 async/await 来书写条件语句会更加直观

    function loadData() {
      return getJSON().then(function(response) {
        if (response.needsAnotherRequest) {
          return makeAnotherRequest(response)
            .then(function(anotherResponse) {
              console.log(anotherResponse)
              return anotherResponse
            })
        } else {
          console.log(response)
          return response
        }
      })
    }
    
    // async
    async function loadData() {
      var response = await getJSON();
      if (response.needsAnotherRequest) {
        var anotherResponse = await makeAnotherRequest(response);
        console.log(anotherResponse)
        return anotherResponse
      } else {
        console.log(response);
        return response;    
      }
    }
    

    async 函数可以保留运行堆栈

    const a = () => {
      b().then(() => c());
    };
    

    上面代码中,函数 a 内部运行了一个异步任务 b,当 b 运行时函数 a 不会中断而是继续执行,等到 b 运行结束时可能 a 早就运行结束了,b 所在的上下文环境已经消失了,若 bc 报错,错误堆栈将不包括a

    现将这个例子改成 async 函数

    const a = async () => {
      await b();
      c();
    };
    

    上面代码中 b 运行时 a 是暂停执行的,上下文环境都保存着,一旦 bc 报错,错误堆栈将包括a

    再看下面例子

    let a = 0;
    let b = async () => {
      a = a + await 10;
      console.log('2', a);
    };
    b();
    a ++; 
    console.log('1', a);
    // '1' 1 
    // '2' 10
    

    上面代码中,首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generatorgenerator 会保留堆栈中的东西,所以这时 a = 0 被保存下来

    因为 await 是异步操作,后面的表达式不返回 Promise 的话就会包装成 Promise.reslove(返回值),然后去执行函数外的同步代码

    函数外的同步代码执行完毕后开始回来执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10 = 10

总结

对于常用的不同异步编程处理方案,各有优势劣势,也没必要顶一个踩一个,虽然技术在不断发展优化,但有些技术不至于淘汰如此之快,存在即合理。个人观点最好的做法是针对不同的业务场景可根据情况选择合适高效的方案