JavaScript高级语法笔记(五):Promise

159 阅读15分钟

Promise

ECMAscript 6 原生提供了 Promise 对象,在一定程度上解决了以往“回调地狱”的问题。

Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。

Promise对象的特点

  • 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:

    • pending: 初始状态,不是成功或失败状态。
    • fulfilled: 意味着操作成功完成。
    • rejected: 意味着操作失败。

    只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。

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

    Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

优缺点

  • 有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。
  • Promise 也有一些缺点。首先,无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

方法

  • then

    Promise.prototype.then 方法返回的是一个新的 Promise 对象,因此可以采用链式写法。

    getJSON("/posts.json").then(function(json) {
        return json.post;
    }).then(function(post) {
        // proceed
    });
    

    上面的代码使用 then 方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

    如果前一个回调函数返回的是Promise对象,这时后一个回调函数就会等待该Promise对象有了运行结果,才会进一步调用。

    getJSON("/post/1.json").then(function(post) {
        return getJSON(post.commentURL);
    }).then(function(comments) {
        // 对comments进行处理
    });
    

    这种设计使得嵌套的异步操作,可以被很容易得改写,从回调函数的"横向发展"改为"向下发展"。

  • catch

    Promise.prototype.catch 方法是 Promise.prototype.then(null, rejection) 的别名,用于指定发生错误时的回调函数。

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

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

    getJSON("/post/1.json").then(function(post) {
        return getJSON(post.commentURL);
    }).then(function(comments) {
        // some code
    }).catch(function(error) {
        // 处理前两个回调函数的错误
    });
    
  • all和race

    Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

    var p = Promise.all([p1,p2,p3]);
    

    上面代码中,Promise.all 方法接受一个数组作为参数,p1、p2、p3 都是 Promise 对象的实例。(Promise.all 方法的参数不一定是数组,但是必须具有 iterator 接口,且返回的每个成员都是 Promise 实例。)

    p 的状态由 p1、p2、p3 决定,分成两种情况。

    • 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
    • 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

    下面是一个具体的例子。

    // 生成一个Promise对象的数组 
    var promises = [2, 3, 5, 7, 11, 13].map(function(id){
        return getJSON("/post/" + id + ".json");
    });
    Promise.all(promises).then(function(posts) {
        // ...
    }).catch(function(reason){
        // ...
    });
    

    Promise.race 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

    var p = Promise.race([p1,p2,p3]);
    

    上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的返回值。

    如果Promise.all方法和Promise.race方法的参数,不是Promise实例,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。

  • resolve和reject

    有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。

    var jsPromise = Promise.resolve($.ajax('/whatever.json'));
    

    上面代码将 jQuery 生成 deferred 对象,转为一个新的 ES6 的 Promise 对象。

    如果 Promise.resolve 方法的参数,不是具有 then 方法的对象(又称 thenable 对象),则返回一个新的 Promise 对象,且它的状态为fulfilled。

    var p = Promise.resolve('Hello');
    p.then(function (s){
        console.log(s)
    }); 
    // Hello
    

    上面代码生成一个新的Promise对象的实例p,它的状态为fulfilled,所以回调函数会立即执行,Promise.resolve方法的参数就是回调函数的参数。

    如果Promise.resolve方法的参数是一个Promise对象的实例,则会被原封不动地返回。

    Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。

    var p = Promise.reject('出错了');
    p.then(null, function (s){
        console.log(s)
    });
    // 出错了
    

    上面代码生成一个Promise对象的实例p,状态为rejected,回调函数会立即执行。

    • finally和allSettled

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

      finally() 方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise是否成功完成后都需要执行的代码提供了一种方式。

      这避免了同样的语句需要在then()catch()中各写一次的情况。

    • Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilledrejected后的promise,并带有一个对象数组,每个对象表示对应的promise结果。

      当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise的结果时,通常使用它。

      相比之下,Promise.all() 更适合彼此相互依赖或者在其中任何一个reject时立即结束。

      Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。

    Promise实现Ajax

    下面是一个用 Promise 对象实现的 Ajax 操作的例子。

    function ajax(URL) {
        return new Promise(function (resolve, reject) {
            var req = new XMLHttpRequest();
            req.open('GET', URL, true);
            req.onload = function () {
                if (req.status === 200) {
                    resolve(req.responseText);
                } else {
                    reject(new Error(req.statusText));
                }
            };
            req.onerror = function () {
                reject(new Error(req.statusText));
            };
            req.send();
        });
    }
    var URL = "/try/ajax/testpromise.php";
    ajax(URL).then(function onFulfilled(value){
        document.write('内容是:' + value);
    }).catch(function onRejected(error){
        document.write('错误:' + error);
    });
    

    上面代码中,resolve 方法和 reject 方法调用时,都带有参数。它们的参数会被传递给回调函数。

    reject 方法的参数通常是 Error 对象的实例,而 resolve 方法的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样。

    var p1 = new Promise(function(resolve, reject){
        // ... some code
    });
    var p2 = new Promise(function(resolve, reject){
        // ... some code  resolve(p1);
    })
    

    上面代码中,p1 和 p2 都是 Promise 的实例,但是 p2 的 resolve 方法将 p1 作为参数,这时 p1 的状态就会传递给 p2。如果调用的时候,p1 的状态是 pending,那么 p2 的回调函数就会等待 p1 的状态改变;如果 p1 的状态已经是 fulfilled 或者 rejected,那么 p2 的回调函数将会立刻执行。

    手写Promise

    class myPromise {
      // 为了统一用static创建静态属性,用来管理状态
      static PENDING = "pending";
      static FULFILLED = "fulfilled";
      static REJECTED = "rejected";
    ​
      // 构造函数:通过new命令生成对象实例时,自动调用类的构造函数
      constructor(func) {
        // 给类的构造方法constructor添加一个参数func
        this.PromiseState = myPromise.PENDING; // 指定Promise对象的状态属性 PromiseState,初始值为pending
        this.PromiseResult = null; // 指定Promise对象的结果 PromiseResult
        this.onFulfilledCallbacks = []; // 保存成功回调
        this.onRejectedCallbacks = []; // 保存失败回调
        try {
          /**
           * func()传入resolve和reject,
           * resolve()和reject()方法在外部调用,这里需要用bind修正一下this指向
           * new 对象实例时,自动执行func()
           */
          func(this.resolve.bind(this), this.reject.bind(this));
        } catch (error) {
          // 生成实例时(执行resolve和reject),如果报错,就把错误信息传入给reject()方法,并且直接执行reject()方法
          this.reject(error);
        }
      }
    ​
      resolve(result) {
        // result为成功态时接收的终值
        // 只能由pedning状态 => fulfilled状态 (避免调用多次resolve reject)
        if (this.PromiseState === myPromise.PENDING) {
          /**
           * 为什么resolve和reject要加setTimeout?
           * 2.2.4规范 onFulfilled 和 onRejected 只允许在 execution context 栈仅包含平台代码时运行.
           * 注1 这里的平台代码指的是引擎、环境以及 promise 的实施代码。实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
           * 这个事件队列可以采用“宏任务(macro-task)”机制,比如setTimeout 或者 setImmediate; 也可以采用“微任务(micro-task)”机制来实现, 比如 MutationObserver 或者process.nextTick。
           */
          setTimeout(() => {
            this.PromiseState = myPromise.FULFILLED;
            this.PromiseResult = result;
            /**
             * 在执行resolve或者reject的时候,遍历自身的callbacks数组,
             * 看看数组里面有没有then那边 保留 过来的 待执行函数,
             * 然后逐个执行数组里面的函数,执行的时候会传入相应的参数
             */
            this.onFulfilledCallbacks.forEach((callback) => {
              callback(result);
            });
          });
        }
      }
    ​
      reject(reason) {
        // reason为拒绝态时接收的终值
        // 只能由pedning状态 => rejected状态 (避免调用多次resolve reject)
        if (this.PromiseState === myPromise.PENDING) {
          setTimeout(() => {
            this.PromiseState = myPromise.REJECTED;
            this.PromiseResult = reason;
            this.onRejectedCallbacks.forEach((callback) => {
              callback(reason);
            });
          });
        }
      }
    ​
      /**
       * [注册fulfilled状态/rejected状态对应的回调函数]
       * @param {function} onFulfilled  fulfilled状态时 执行的函数
       * @param {function} onRejected  rejected状态时 执行的函数
       * @returns {function} newPromsie  返回一个新的promise对象
       */
      then(onFulfilled, onRejected) {
        /**
         * 参数校验:Promise规定then方法里面的两个参数如果不是函数的话就要被忽略
         * 所谓“忽略”并不是什么都不干,
         * 对于onFulfilled来说“忽略”就是将value原封不动的返回,
         * 对于onRejected来说就是返回reason,
         *     onRejected因为是错误分支,我们返回reason应该throw一个Error
         */
        onFulfilled =
          typeof onFulfilled === "function" ? onFulfilled : (value) => value;
        onRejected =
          typeof onRejected === "function"
            ? onRejected
            : (reason) => {
                throw reason;
              };
    ​
        // 2.2.7规范 then 方法必须返回一个 promise 对象
        let promise2 = new myPromise((resolve, reject) => {
          if (this.PromiseState === myPromise.FULFILLED) {
            /**
             * 为什么这里要加定时器setTimeout?
             * 2.2.4规范 onFulfilled 和 onRejected 只有在执行环境堆栈仅包含平台代码时才可被调用 注1
             * 这里的平台代码指的是引擎、环境以及 promise 的实施代码。
             * 实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
             * 这个事件队列可以采用“宏任务(macro-task)”机制,比如setTimeout 或者 setImmediate; 也可以采用“微任务(micro-task)”机制来实现, 比如 MutationObserver 或者process.nextTick。
             */
            setTimeout(() => {
              try {
                // 2.2.7.1规范 如果 onFulfilled 或者 onRejected 返回一个值 x ,则运行下面的 Promise 解决过程:[[Resolve]](promise2, x),即运行resolvePromise()
                let x = onFulfilled(this.PromiseResult);
                resolvePromise(promise2, x, resolve, reject);
              } catch (e) {
                // 2.2.7.2 如果 onFulfilled 或者 onRejected 抛出一个异常 e ,则 promise2 必须拒绝执行,并返回拒因 e
                reject(e); // 捕获前面onFulfilled中抛出的异常
              }
            });
          } else if (this.PromiseState === myPromise.REJECTED) {
            setTimeout(() => {
              try {
                let x = onRejected(this.PromiseResult);
                resolvePromise(promise2, x, resolve, reject);
              } catch (e) {
                reject(e);
              }
            });
          } else if (this.PromiseState === myPromise.PENDING) {
            // pending 状态保存的 resolve() 和 reject() 回调也要符合 2.2.7.1 和 2.2.7.2 规范
            this.onFulfilledCallbacks.push(() => {
              try {
                let x = onFulfilled(this.PromiseResult);
                resolvePromise(promise2, x, resolve, reject);
              } catch (e) {
                reject(e);
              }
            });
            this.onRejectedCallbacks.push(() => {
              try {
                let x = onRejected(this.PromiseResult);
                resolvePromise(promise2, x, resolve, reject);
              } catch (e) {
                reject(e);
              }
            });
          }
        });
    ​
        return promise2;
      }
    ​
      // catch行为与调用Promise.prototype.then(undefined, onRejected) 相同
      catch(onRejected) {
        return this.then(undefined, onRejected);
      }
    ​
      // 在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。
      finally(onFinally) {
        this.then(
          () => {
            onFinally();
          },
          () => {
            onFinally();
          }
        );
      }
    ​
      // 返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。
      static race(promises) {
        return new myPromise((resolve, reject) => {
          promises.forEach((promise) => {
            promise.then(resolve, reject);
          });
        });
      }
    ​
      static all(promises) {
        return new myPromise((resolve, reject) => {
          const values = [];
          promises.forEach((promise) => {
            promise.then(
              (res) => {
                values.push(res);
                if (values.length === promises.length) {
                  resolve(values);
                }
              },
              (err) => {
                reject(err);
              }
            );
          });
        });
      }
    ​
      // 返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的promise结果
      static allSettled(promises) {
        return new myPromise((resolve) => {
          const results = [];
          promises.forEach((promise) => {
            promise.then(
              (res) => {
                results.push({ status: myPromise.FULFILLED, value: res });
                if (results.length === promises.length) {
                  resolve(results);
                }
              },
              (err) => {
                results.push({ status: myPromise.REJECTED, reason: err });
                if (results.length === promises.length) {
                  resolve(results);
                }
              }
            );
          });
        });
      }
    ​
      /**
       * 接收一个Promise可迭代对象,只要其中的一个 promise 成功,就返回那个已经成功的 promise
       * 如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promise 和AggregateError类型的实例
       */
      static any(promises) {
        // resolve必须等到有一个成功的结果
        // reject所有的都失败才执行reject
        const reasons = [];
        return new myPromise((resolve, reject) => {
          promises.forEach((promise) => {
            promise.then(resolve, (err) => {
              reasons.push(err);
              if (reasons.length === promises.length) {
                reject(new AggregateError(reasons));
              }
            });
          });
        });
      }
    }
    ​
    /**
     * 对resolve()、reject() 进行改造增强 针对resolve()和reject()中不同值情况 进行处理
     * @param  {promise} promise2 promise1.then方法返回的新的promise对象
     * @param  {[type]} x         promise1中onFulfilled或onRejected的返回值
     * @param  {[type]} resolve   promise2的resolve方法
     * @param  {[type]} reject    promise2的reject方法
     */
    function resolvePromise(promise2, x, resolve, reject) {
      // 2.3.1规范 如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promise
      if (x === promise2) {
        return reject(new TypeError("Chaining cycle detected for promise"));
      }
    ​
      // 2.3.2规范 如果 x 为 Promise ,则使 promise2 接受 x 的状态
      if (x instanceof myPromise) {
        if (x.PromiseState === myPromise.PENDING) {
          /**
           * 2.3.2.1 如果 x 处于等待态, promise 需保持为等待态直至 x 被执行或拒绝
           *         注意"直至 x 被执行或拒绝"这句话,
           *         这句话的意思是:x 被执行x,如果执行的时候拿到一个y,还要继续解析y
           */
          x.then((y) => {
            resolvePromise(promise2, y, resolve, reject);
          }, reject);
        } else if (x.PromiseState === myPromise.FULFILLED) {
          // 2.3.2.2 如果 x 处于执行态,用相同的值执行 promise
          resolve(x.PromiseResult);
        } else if (x.PromiseState === myPromise.REJECTED) {
          // 2.3.2.3 如果 x 处于拒绝态,用相同的据因拒绝 promise
          reject(x.PromiseResult);
        }
      } else if (
        x !== null &&
        (typeof x === "object" || typeof x === "function")
      ) {
        // 2.3.3 如果 x 为对象或函数
        try {
          // 2.3.3.1 把 x.then 赋值给 then
          var then = x.then;
        } catch (e) {
          // 2.3.3.2 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise
          return reject(e);
        }
    ​
        /**
         * 2.3.3.3
         * 如果 then 是函数,将 x 作为函数的作用域 this 调用之。
         * 传递两个回调函数作为参数,
         * 第一个参数叫做 `resolvePromise` ,第二个参数叫做 `rejectPromise`
         */
        if (typeof then === "function") {
          // 2.3.3.3.3 如果 resolvePromise 和 rejectPromise 均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
          let called = false; // 避免多次调用
          try {
            then.call(
              x,
              // 2.3.3.3.1 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y)
              (y) => {
                if (called) return;
                called = true;
                resolvePromise(promise2, y, resolve, reject);
              },
              // 2.3.3.3.2 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise
              (r) => {
                if (called) return;
                called = true;
                reject(r);
              }
            );
          } catch (e) {
            /**
             * 2.3.3.3.4 如果调用 then 方法抛出了异常 e
             * 2.3.3.3.4.1 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略之
             */
            if (called) return;
            called = true;
    ​
            /**
             * 2.3.3.3.4.2 否则以 e 为据因拒绝 promise
             */
            reject(e);
          }
        } else {
          // 2.3.3.4 如果 then 不是函数,以 x 为参数执行 promise
          resolve(x);
        }
      } else {
        // 2.3.4 如果 x 不为对象或者函数,以 x 为参数执行 promise
        return resolve(x);
      }
    }
    

迭代器和生成器

迭代器(Iterator)

  • 概念

    • 迭代器是确使用户在容器对象(链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。

    • 迭代器的定义可能比较抽象,简单来说迭代器就是一个对象,可用于帮助我们对某个数据结构(链表、数组)进行遍历

    • 在JavaScript中,迭代器也是一个具体的对象,并且这个对象必须符合迭代器协议(iterator protocol);

    • 什么是迭代器协议?

      • 在JavaScript中就是指这个对象必须实现一个特定的next方法

      • next方法可接收0个或者1个参数(在生成器中next可以接收1个参数),并且需返回一个对象,对象包含以下两个属性:

        • done:值为Boolean,如果迭代器可以迭代产生下一个值,就为false,如果已经迭代完毕,就为true;
        • value:迭代器返回的值,如果done为true,value一般为undefined;
  • 基本使用

    • 创建一个迭代器对象

      const names = ['curry', 'kobe', 'klay']
      ​
      let index = 0 // 通过一个index来记录当前访问的位置
      const iterator = {
        next() {
          if (index < names.length) {
            return { done: false, value: names[index++] }
          } else {
            return { done: true, value: undefined }
          }
        }
      }
      ​
      console.log(iterator.next()) // { done: false, value: 'curry' }
      console.log(iterator.next()) // { done: false, value: 'kobe' }
      console.log(iterator.next()) // { done: false, value: 'klay' }
      console.log(iterator.next()) // { done: true, value: undefined }
      
    • 实现生成迭代器的函数

      • 如果每次需要去访问一个数组就去编写一个对应的迭代器对象肯定是很麻烦的;
      • 可以封装一个函数,用于生成一个访问数组的迭代器;
      function createIterator(arr) {
        let index = 0
        return {
          next() {
            if (index < arr.length) {
              return { done: false, value: arr[index++] }
            } else {
              return { done: true, value: undefined }
            }
          }
        }
      }
      ​
      const names = ['curry', 'kobe', 'klay']
      // 调用createIterator函数,生成一个访问names数组的迭代器
      const namesIterator = createIterator(names)
      ​
      console.log(namesIterator.next()) // { done: false, value: 'curry' }
      console.log(namesIterator.next()) // { done: false, value: 'kobe' }
      console.log(namesIterator.next()) // { done: false, value: 'klay' }
      console.log(namesIterator.next()) // { done: true, value: undefined }
      
  • 可迭代对象

    • 概念

      • 迭代器是一个符合迭代器协议(iterator protocol) 的对象,对象内实现了一个特定的next方法;
      • 而可迭代对象是一个符合可迭代协议(iterable protocol) 的对象,对象内实现了一个Symbol.iterator方法,并且该方法返回一个迭代器对象;
      • 所以,可以说可迭代对象包含了迭代器对象,可迭代对象中实现了一个特定方法用于返回迭代器对象;

      如下,iteratorObj就是一个可迭代对象:

      const iteratorObj = {
        names: ['curry', 'kobe', 'klay'],
        [Symbol.iterator]: function() {
          let index = 0
          return {
            // 注意:这里的next需要使用箭头函数,否则this访问不到iteratorObj
            next: () => {
              if (index < this.names.length) {
                return { done: false, value: this.names[index++] }
              } else {
                return { done: true, value: undefined }
              }
            }
          }
        }
      }
      
    • JS内置的可迭代对象

      • String、Array、Map、Set、arguments对象、NodeList(DOM集合)等。
    • 可迭代对象的应用场景

      • JS中的语法:for...of、展开语法、解构等。

        • for...of可用于遍历一个可迭代对象,其原理就是利用迭代器的next函数,如果done为false,就从返回的对象中拿到value返回,而对象不是一个可迭代对象,所以对象不能使用for...of遍历;
        • 可迭代对象都是可以使用解构语法的。例如数组的使用展开语法,其原理也是用到了迭代器,在使用...对数组进行展开时,也是通过迭代器的next去获取数组的每一项值,然后存放到新数组中;
        • 注意:在扩展语法和解构语法中,对象的扩展和解构是在ES9中新增的特性,其原理并不是使用迭代器实现的,只是ECMA提供给我们的一种操作对象的新语法而已;
      • JS创建对象:new Map([Iterable])、new WeakMap([Iterable])、new Set([Iterable])、new WeakSet([Iterable])等。

        // 1.Set
        const set = new Set(iteratorObj)
        console.log(set) // Set(3) { 'curry', 'kobe', 'klay' }// 2.Array.from
        const names = Array.from(iteratorObj)
        console.log(names) // [ 'curry', 'kobe', 'klay' ]
        
      • JS方法调用:Promise.all(Iterable)、Promise.race(Iterable)、Array.from(Iterable)等。

        // 传入的可迭代对象中的每个值,会使用Promise.resolve进行包裹
        Promise.all(iteratorObj).then(res => {
          console.log(res) // [ 'curry', 'kobe', 'klay' ]
        })
        
    • 自定义可迭代类

      对象不是一个可迭代对象,所以对象不能使用for...of遍历,如果我们想要实现通过for...of遍历对象呢?那么可以自己实现一个类,这个类的实例化对象是可迭代对象。

      • 实现一个Person类,并且Person类中实现了Symbol.iterator方法用于返回一个迭代器;
      • Person类的实例化对象p中包含一个friends数组,通过for...of遍历p对象时,可以将friends数组的每一项遍历出来;
      class Person {
        constructor(name, age, friends) {
          this.name = name
          this.age = age
          this.friends = friends
        }
      ​
        [Symbol.iterator]() {
          let index = 0
          return {
            next: () => {
              if (index < this.friends.length) {
                return { done: false, value: this.friends[index++] }
              } else {
                return { done: true, value: undefined }
              }
            }
          }
        }
      }
      ​
      const p = new Person('curry', 30, ['kobe', 'klay', 'green'])
      for (const name of p) {
        console.log(name) // kobe klay green
      }
      

生成器(Generator)

  • 概念

    • 生成器是ES6中新增的一种控制函数执行的方案,它可以帮助我们控制函数的暂停和执行。生成器是一种特殊的迭代器,所以生成器也是一个对象,并且可以调用next方法。

    • 创建生成器对象需要使用生成器函数,生成器函数和普通函数不一样,主要有以下特点:

      • 生成器函数的声明需要在function后加上一个符号*
      • 在生成器函数中可以使用yield关键字来分割函数体代码,控制函数的执行;
      • 生成器函数调用的返回值就是生成器对象了;
  • 基本使用

    function* generatorFn() {
      console.log('函数开始执行~')
    ​
      console.log('函数第一段代码执行...')
      yield
      console.log('函数第二段代码执行...')
      yield
      console.log('函数第三段代码执行...')
      yield
      console.log('函数第四段代码执行...')
    ​
      console.log('函数执行结束~')
    }
    ​
    // 调用generatorFn获取生成器
    const generator = generatorFn()
    ​
    generator.next() // 函数第一段代码执行...
    console.log('------------------------')
    generator.next() // 函数第二段代码执行...
    console.log('------------------------')
    generator.next() // 函数第三段代码执行...
    console.log('------------------------')
    generator.next() // 函数第四段代码执行...
  • 生成器next方法的返回值

    next方法的返回值是一个包含done、value属性的对象。

    function* generatorFn() {
      console.log('函数第一段代码执行...')
      yield
      console.log('函数第二段代码执行...')
      yield
      console.log('函数第三段代码执行...')
      yield
      console.log('函数第四段代码执行...')
    }
    ​
    // 调用generatorFn获取生成器
    const generator = generatorFn()
    ​
    console.log(generator.next()) // {value: undefined, done: false}
    console.log('------------------------')
    console.log(generator.next()) // {value: undefined, done: false}
    console.log('------------------------')
    console.log(generator.next()) // {value: undefined, done: false}
    console.log('------------------------')
    console.log(generator.next()) // {value: undefined, done: true}
    

    如果需要指定next返回值中的value,那么可以通过在yield后面跟上一个值或者表达式,就可以将对应的值传递到next返回对象value中了。

    function* generatorFn() {
      console.log('函数第一段代码执行...')
      yield 10
      console.log('函数第二段代码执行...')
      yield 20
      console.log('函数第三段代码执行...')
      yield 30
      console.log('函数第四段代码执行...')
    }
    ​
    // 调用generatorFn获取生成器
    const generator = generatorFn()
    ​
    console.log(generator.next()) // {value: 10, done: false}
    console.log('------------------------') // {value: 20, done: false}
    console.log(generator.next())
    console.log('------------------------') // {value: 30, done: false}
    console.log(generator.next())
    console.log('------------------------') // {value: undefined, done: true}
    console.log(generator.next())
    

    在执行完第四段代码后,调用的next返回值为{ value: undefined, done: true },原因是后面已经没有yield了,而且当函数没有指定返回值时,最后会默认执行return undefined

    • 生成器next方法的参数传递

    return方法也可以给生成器函数传递参数,但是调用return后,生成器函数就会中断,之后再调用next就不会再继续生成值了。

  • 生成器的throw方法

    throw方法可以给生成器函数内部抛出异常。

    • 生成器调用throw方法抛出异常后,可以在生成器函数中进行捕获;
    • 通过try catch捕获异常后,后续的代码还是可以正常执行的;
  • 生成器替换迭代器

    • 方式一:根据数组元素的个数,执行yield;

      function* createIterator(arr) {
        let index = 0
        yield arr[index++]
        yield arr[index++]
        yield arr[index++]
      }
      
    • 方式二:遍历数组,执行yield;

      function* createIterator(arr) {
        for (const item of arr) {
          yield item
        }
      }
      
    • 方式三:执行yield*,后面可以跟上一个可迭代对象,它会依次迭代其中每一个值;

      function* createIterator(arr) {
        yield* arr
      }
      

    测试一下以上三种方法,执行结果都是一样的:

    const names = ['curry', 'kobe', 'klay']
    const iterator = createIterator(names)
    ​
    console.log(iterator.next()) // { value: 'curry', done: false }
    console.log(iterator.next()) // { value: 'kobe', done: false }
    console.log(iterator.next()) // { value: 'klay', done: false }
    console.log(iterator.next()) // { value: undefined, done: true }
    

异步请求的处理方案

在进行异步请求时,如果出现了这样一个需求,下一次的请求需要拿到上一次请求的结果。如下是使用Promise封装的一个request方法。

function request(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(url)
    }, 300)
  })
}
  • 方案一:使用Promise的then进行嵌套调用

    • 在then中拿到结果后,再去做下一次请求,以此类推;
    • 缺点:形成了回调地狱;
    request('/url').then(res => {
      request(res + '/bbb').then(res => {
        request(res + '/ccc').then(res => {
          console.log(res) // /aaa/bbb/ccc
        })
      })
    })
    
  • 方案二:使用Promise的then的返回值

    • 虽然可以解决回调地狱问题,但是阅读性不佳;
    request('/url').then(res => {
      return request(res + '/bbb')
    }).then(res => {
      return request(res + '/ccc')
    }).then(res => {
      console.log(res) // /aaa/bbb/ccc
    })
    
  • 方案三:使用Promise和Generator处理

    function* getRequestData() {
      const res1 = yield request('/aaa')
      const res2 = yield request(res1 + '/bbb')
      const res3 = yield request(res2 + '/ccc')
      console.log(res3)
    }
    
    • 手动执行生成器的next方法;
    const generator = getRequestData()
    generator.next().value.then(res => {
      generator.next(res).value.then(res => {
        generator.next(res).value.then(res => {
          generator.next(res) // /aaa/bbb/ccc
        })
      })
    })
    
    • 自动执行生成器的next方法:如果手动执行嵌套层级过多的话是不方便的,那么可以借助递归的思想实现一个自动执行生成器的函数;
    function autoGenerator(generatorFn) {
      const generator = generatorFn()
    ​
      function recursion(res) {
        const result = generator.next(res)
        // 如果done值为true,说明结束了
        if (result.done) return result.value
        // 没有结束,继续调用Promise的then
        result.value.then(res => {
          recursion(res)
        })
      }
    ​
      recursion()
    }
    ​
    autoGenerator(getRequestData) // /aaa/bbb/ccc
    
    • 使用第三方库来执行生成器:像自动执行生成器函数,早就已经有第三方库帮助我们实现了,如co
  • 方案四:使用async和await

    async和await是我们解决异步回调的最终解决方案,它可以让我们异步的代码,看上去是同步执行的。

    async function getRequestData() {
      const res1 = await request('/aaa')
      const res2 = await request(res1 + '/bbb')
      const res3 = await request(res2 + '/ccc')
      console.log(res3)
    }
    ​
    getRequestData() // /aaa/bbb/ccc
    

async和await的原理

  • async、await和生成器的关系:

    • 将生成器函数的*换成async
    • 将生成器函数中的yield换成await
    • 两种方案所体现出的效果和代码书写形式几乎差不多;
  • 总结

    • async和await的原理其实就是Promise+生成器实现的;
    • 为什么async、await能够让异步代码看上去是同步执行的,其原因就在于生成器的next方法可以对函数内代码的执行进行控制,当上一次请求拿到结果后,再去执行下一次next;
    • 所以为什么说async和await只是Promise+生成器的语法糖,其原理就在这;

Promise面试

event loop

  • 执行顺序

    • 一开始整个脚本作为一个宏任务执行
    • 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
    • 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完
    • 执行浏览器UI线程的渲染工作
    • 检查是否有Web Worker任务,有则执行
    • 执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
  • 微任务包括: MutationObserverPromise.then()或catch()Promise为基础开发的其它技术,比如fetch APIV8的垃圾回收过程、Node独有的process.nextTick

  • 宏任务包括scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

  • 注意⚠️:在所有任务开始的时候,由于宏任务中包括了script,所以浏览器会先执行一个宏任务,在这个过程中你看到的延迟任务(例如setTimeout)将被放到下一轮宏任务中来执行。

基础题

  • 题目一

    const promise1 = new Promise((resolve, reject) => {
      console.log('promise1')
    })
    console.log('1', promise1);
    

    过程分析:

    • 从上至下,先遇到new Promise,执行该构造函数中的代码promise1
    • 然后执行同步代码1,此时promise1没有被resolve或者reject,因此状态还是pending

    结果:

    'promise1'
    '1' Promise{<pending>}
    
  • 题目二

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

    过程分析:

    • 从上至下,先遇到new Promise,执行其中的同步代码1
    • 再遇到resolve('success'), 将promise的状态改为了resolved并且将值保存下来
    • 继续执行同步代码2
    • 跳出promise,往下执行,碰到promise.then这个微任务,将其加入微任务队列
    • 执行同步代码4
    • 本轮宏任务全部执行完毕,检查微任务队列,发现promise.then这个微任务且状态为resolved,执行它。

    结果:

    1 2 4 3
    
  • 题目三

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

    过程分析

    • 和题目二相似,只不过在promise中并没有resolve或者reject
    • 因此promise.then并不会执行,它只有在被改变了状态之后才会执行。

    结果:

    1 2 4
    
  • 题目四

    const promise1 = new Promise((resolve, reject) => {
      console.log('promise1')
      resolve('resolve1')
    })
    const promise2 = promise1.then(res => {
      console.log(res)
    })
    console.log('1', promise1);
    console.log('2', promise2);
    

    过程分析:

    • 从上至下,先遇到new Promise,执行该构造函数中的代码promise1
    • 碰到resolve函数, 将promise1的状态改变为resolved, 并将结果保存下来
    • 碰到promise1.then这个微任务,将它放入微任务队列
    • promise2是一个新的状态为pendingPromise
    • 执行同步代码1, 同时打印出promise1的状态是resolved
    • 执行同步代码2,同时打印出promise2的状态是pending
    • 宏任务执行完毕,查找微任务队列,发现promise1.then这个微任务且状态为resolved,执行它。

    结果:

    'promise1'
    '1' Promise{<resolved>: 'resolve1'}
    '2' Promise{<pending>}
    'resolve1'
    
  • 题目五

    const fn = () => (new Promise((resolve, reject) => {
      console.log(1);
      resolve('success')
    }))
    fn().then(res => {
      console.log(res)
    })
    console.log('start')
    

    fn函数它是直接返回了一个new Promise的,而且fn函数的调用是在start之前,所以它里面的内容应该会先执行。

    结果:

    1
    'start'
    'success'
    

如果把fn的调用放到start之后,start就在1之前打印出来了,因为fn函数是之后执 行的。

Promise结合setTimeout

  • 题目一

    console.log('start')
    setTimeout(() => {
      console.log('time')
    })
    Promise.resolve().then(() => {
      console.log('resolve')
    })
    console.log('end')
    

    过程分析:

    • 刚开始整个脚本作为一个宏任务来执行,对于同步代码直接压入执行栈进行执行,因此先打印出startend
    • setTimout作为一个宏任务被放入宏任务队列(下一个)
    • Promise.then作为一个微任务被放入微任务队列
    • 本次宏任务执行完,检查微任务,发现Promise.then,执行它
    • 接下来进入下一个宏任务,发现setTimeout,执行。

    结果:

    'start'
    'end'
    'resolve'
    'time'
    
  • 题目二:

    const promise = new Promise((resolve, reject) => {
      console.log(1);
      setTimeout(() => {
        console.log("timerStart");
        resolve("success");
        console.log("timerEnd");
      }, 0);
      console.log(2);
    });
    promise.then((res) => {
      console.log(res);
    });
    console.log(4);
    

    过程分析:

    • 从上至下,先遇到new Promise,执行该构造函数中的代码1
    • 然后碰到了定时器,将这个定时器中的函数放到下一个宏任务的延迟队列中等待执行
    • 执行同步代码2
    • 跳出promise函数,遇到promise.then,但其状态还是为pending,这里理解为先不执行
    • 执行同步代码4
    • 一轮循环过后,进入第二次宏任务,发现延迟队列中有setTimeout定时器,执行它
    • 首先执行timerStart,然后遇到了resolve,将promise的状态改为resolved且保存结果并将之前的promise.then推入微任务队列
    • 继续执行同步代码timerEnd
    • 宏任务全部执行完毕,查找微任务队列,发现promise.then这个微任务,执行它。

    执行结果为:

    1
    2
    4
    "timerStart"
    "timerEnd"
    "success"
    
  • 题目三

    (1)

    setTimeout(() => {
      console.log('timer1');
      setTimeout(() => {
        console.log('timer3')
      }, 0)
    }, 0)
    setTimeout(() => {
      console.log('timer2')
    }, 0)
    console.log('start')
    

    (2)

    setTimeout(() => {
      console.log('timer1');
      Promise.resolve().then(() => {
        console.log('promise')
      })
    }, 0)
    setTimeout(() => {
      console.log('timer2')
    }, 0)
    console.log('start')
    

    执行结果:

    'start'
    'timer1'
    'timer2'
    'timer3'
    
    'start'
    'timer1'
    'promise'
    'timer2'
    

    这两个例子,看着好像只是把第一个定时器中的内容换了一下而已。

    一个是为定时器timer3,一个是为Promise.then

    但是如果是定时器timer3的话,它会在timer2后执行,而Promise.then却是在timer2之前执行。

    你可以这样理解,Promise.then是微任务,它会被加入到本轮中的微任务列表,而定时器timer3是宏任务,它会被加入到下一轮的宏任务中。

    理解完这两个案例,可以来看看下面一道比较难的题目了。

    Promise.resolve().then(() => {
      console.log('promise1');
      const timer2 = setTimeout(() => {
        console.log('timer2')
      }, 0)
    });
    const timer1 = setTimeout(() => {
      console.log('timer1')
      Promise.resolve().then(() => {
        console.log('promise2')
      })
    }, 0)
    console.log('start');
    

    过程分析为:

    • 刚开始整个脚本作为第一次宏任务来执行,我们将它标记为宏1,从上至下执行
    • 遇到Promise.resolve().then这个微任务,将then中的内容加入第一次的微任务队列标记为微1
    • 遇到定时器timer1,将它加入下一次宏任务的延迟列表,标记为宏2,等待执行(先不管里面是什么内容)
    • 执行宏1中的同步代码start
    • 第一次宏任务(宏1)执行完毕,检查第一次的微任务队列(微1),发现有一个promise.then这个微任务需要执行
    • 执行打印出微1中同步代码promise1,然后发现定时器timer2,将它加入宏2的后面,标记为宏3
    • 第一次微任务队列(微1)执行完毕,执行第二次宏任务(宏2),首先执行同步代码timer1
    • 然后遇到了promise2这个微任务,将它加入此次循环的微任务队列,标记为微2
    • 宏2中没有同步代码可执行了,查找本次循环的微任务队列(微2),发现了promise2,执行它
    • 第二轮执行完毕,执行宏3,打印出timer2
  • 题目四

    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)
    

    过程分析:

    • 从上至下,先执行第一个new Promise中的函数,碰到setTimeout将它加入下一个宏任务列表
    • 跳出new Promise,碰到promise1.then这个微任务,但其状态还是为pending,这里理解为先不执行
    • promise2是一个新的状态为pendingPromise
    • 执行同步代码console.log('promise1'),且打印出的promise1的状态为pending
    • 执行同步代码console.log('promise2'),且打印出的promise2的状态为pending
    • 碰到第二个定时器,将其放入下一个宏任务列表
    • 第一轮宏任务执行结束,并且没有微任务需要执行,因此执行第二轮宏任务
    • 先执行第一个定时器里的内容,将promise1的状态改为resolved且保存结果并将之前的promise1.then推入微任务队列
    • 该定时器中没有其它的同步代码可执行,因此执行本轮的微任务队列,也就是promise1.then,它抛出了一个错误,且将promise2的状态设置为了rejected
    • 第一个定时器执行完毕,开始执行第二个定时器中的内容
    • 打印出'promise1',且此时promise1的状态为resolved
    • 打印出'promise2',且此时promise2的状态为`rejected
    'promise1' Promise{<pending>}
    'promise2' Promise{<pending>}
    Uncaught (in promise) Error: error!!!
    'promise1' Promise{<resolved>: "success"}
    'promise2' Promise{<rejected>: Error: error!!!}
    
  • 题目五

    const promise1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("success");
        console.log("timer1");
      }, 1000);
      console.log("promise1里的内容");
    });
    const promise2 = promise1.then(() => {
      throw new Error("error!!!");
    });
    console.log("promise1", promise1);
    console.log("promise2", promise2);
    setTimeout(() => {
      console.log("timer2");
      console.log("promise1", promise1);
      console.log("promise2", promise2);
    }, 2000);
    

    结果:

    'promise1里的内容'
    'promise1' Promise{<pending>}
    'promise2' Promise{<pending>}
    'timer1'
    Uncaught (in promise) Error: error!!!
    'timer2'
    'promise1' Promise{<resolved>: "success"}
    'promise2' Promise{<rejected>: Error: error!!!}
    

Promise中的then、catch、finally

  • 总结:

    • Promise的状态一经改变就不能再改变。
    • .then.catch都会返回一个新的Promise
    • catch不管被连接到哪里,都能捕获上层未捕捉过的错误。
    • Promise中,返回任意一个非 promise 的值都会被包裹成 promise 对象,例如return 2会被包装为return Promise.resolve(2)
    • Promise.then 或者 .catch 可以被调用多次, 但如果Promise内部的状态一经改变,并且有了一个值,那么后续每次调用.then或者.catch的时候都会直接拿到该值。(见3.5)
    • .then 或者 .catchreturn 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获。(见3.6)
    • .then.catch 返回的值不能是 promise 本身,否则会造成死循环。
    • .then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传。
    • .then方法是能接收两个参数的,第一个是处理成功的函数,第二个是处理失败的函数,再某些时候你可以认为catch.then第二个参数的简便写法。
    • .finally方法也是返回一个Promise,他在Promise结束的时候,无论结果为resolved还是rejected,都会执行里面的回调函数。
  • 题目一

    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"
    

    构造函数中的 resolvereject 只有第一次执行有效,多次调用没有任何作用 。验证了第一个结论,Promise的状态一经改变就不能再改变。

  • 题目二

    const promise = new Promise((resolve, reject) => {
      reject("error");
      resolve("success2");
    });
    promise
    .then(res => {
        console.log("then1: ", res);
      }).then(res => {
        console.log("then2: ", res);
      }).catch(err => {
        console.log("catch: ", err);
      }).then(res => {
        console.log("then3: ", res);
      }
    

    结果:

    "catch: " "error"
    "then3: " undefined
    

    验证了第三个结论,catch不管被连接到哪里,都能捕获上层未捕捉过的错误。

    至于then3也会被执行,那是因为catch()也会返回一个Promise,且由于这个Promise没有返回值,所以打印出来的是undefined

  • 题目三

    Promise.resolve(1)
      .then(res => {
        console.log(res);
        return 2;
      })
      .catch(err => {
        return 3;
      })
      .then(res => {
        console.log(res);
      });
    

    结果:

    1
    2
    

    Promise可以链式调用,不过promise 每次调用 .then 或者 .catch 都会返回一个新的 promise,从而实现了链式调用, 它并不像一般我们任务的链式调用一样return this

    上面的输出结果之所以依次打印出12,那是因为resolve(1)之后走的是第一个then方法,并没有走catch里,所以第二个then中的res得到的实际上是第一个then的返回值。

    return 2会被包装成resolve(2)

  • 题目四

    如果把题目三中的Promise.resolve(1)改为Promise.reject(1)又会怎么样呢?

    Promise.reject(1)
      .then(res => {
        console.log(res);
        return 2;
      })
      .catch(err => {
        console.log(err);
        return 3
      })
      .then(res => {
        console.log(res);
      });
    

    结果:

    1
    3
    
  • 题目五

    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log('timer')
        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)
    })
    

    执行结果:

    'timer'
    'success' 1001
    'success' 1002
    

    当然,如果足够快的话,也可能两个都是1001

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

  • 题目六

    Promise.resolve().then(() => {
      return new Error('error!!!')
    }).then(res => {
      console.log("then: ", res)
    }).catch(err => {
      console.log("catch: ", err)
    })
    

    结果:

    "then: " "Error: error!!!"
    

    返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的return new Error('error!!!')也被包裹成了return Promise.resolve(new Error('error!!!'))

    当然如果你抛出一个错误的话,可以用下面的任意一种:

    return Promise.reject(new Error('error!!!'));
    // or
    throw new Error('error!!!')
    
  • 题目七

    const promise = Promise.resolve().then(() => {
      return promise;
    })
    promise.catch(console.err)
    

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

    因此结果会报错:

    Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
    
  • 题目八

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

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

    第一个then和第二个then中传入的都不是函数,一个是数字类型,一个是对象类型,因此发生了透传,将resolve(1) 的值直接传到最后一个then里。

    所以输出结果为:

    1
    
  • 题目九

    Promise.reject('err!!!')
      .then((res) => {
        console.log('success', res)
      }, (err) => {
        console.log('error', err)
      }).catch(err => {
        console.log('catch', err)
      })
    

    执行结果是:

    'error' 'error!!!'
    

    它进入的是then()中的第二个参数里面,而如果把第二个参数去掉,就进入了catch()中:

    Promise.reject('error!!!')
      .then((res) => {
        console.log('success', res)
      }).catch(err => {
        console.log('catch', err)
      })
    

    执行结果:

    'catch' 'error!!!'
    

    但如果是下面的例子呢?

    Promise.resolve()
      .then(function success (res) {
        throw new Error('error!!!')
      }, function fail1 (err) {
        console.log('fail1', err)
      }).catch(function fail2 (err) {
        console.log('fail2', err)
      })
    

    由于Promise调用的是resolve(),因此.then()执行的应该是success()函数,可是success()函数抛出的是一个错误,它会被后面的catch()给捕获到,而不是被fail1函数捕获。

    因此执行结果为:

    fail2 Error: error!!!
          at success
    
  • 题目十

    function promise1 () {
      let p = new Promise((resolve) => {
        console.log('promise1');
        resolve('1')
      })
      return p;
    }
    function promise2 () {
      return new Promise((resolve, reject) => {
        reject('error')
      })
    }
    promise1()
      .then(res => console.log(res))
      .catch(err => console.log(err))
      .finally(() => console.log('finally1'))
    ​
    promise2()
      .then(res => console.log(res))
      .catch(err => console.log(err))
      .finally(() => console.log('finally2'))
    ​
    

    执行过程:

    • 首先定义了两个函数promise1promise2,先不管接着往下看。
    • promise1函数先被调用了,然后执行里面new Promise的同步代码打印出promise1
    • 之后遇到了resolve(1),将p的状态改为了resolved并将结果保存下来。
    • 此时promise1内的函数内容已经执行完了,跳出该函数
    • 碰到了promise1().then(),由于promise1的状态已经发生了改变且为resolved因此将promise1().then()这条微任务加入本轮的微任务列表(这是第一个微任务)
    • 这时候要注意了,代码并不会接着往链式调用的下面走,也就是不会先将.finally加入微任务列表,那是因为.then本身就是一个微任务,它链式后面的内容必须得等当前这个微任务执行完才会执行,因此这里我们先不管.finally()
    • 再往下走碰到了promise2()函数,其中返回的new Promise中并没有同步代码需要执行,所以执行reject('error')的时候将promise2函数中的Promise的状态变为了rejected
    • 跳出promise2函数,遇到了promise2().catch(),将其加入当前的微任务队列(这是第二个微任务),且链式调用后面的内容得等该任务执行完后才执行,和.then()一样。
    • OK, 本轮的宏任务全部执行完了,来看看微任务列表,存在promise1().then(),执行它,打印出1,然后遇到了.finally()这个微任务将它加入微任务列表(这是第三个微任务)等待执行
    • 再执行promise2().catch()打印出error,执行完后将finally2加入微任务加入微任务列表(这是第四个微任务)
    • OK, 本轮又全部执行完了,但是微任务列表还有两个新的微任务没有执行完,因此依次执行finally1finally2

    结果:

    'promise1'
    '1'
    'error'
    'finally1'
    'finally2'
    

    链式调用后面的内容需要等前一个调用执行完才会执行。

    就像是这里的finally()会等promise1().then()执行完才会将finally()加入微任务队列,其实如果这道题中你把finally()换成是then()也是这样的:

    function promise1 () {
      let p = new Promise((resolve) => {
        console.log('promise1');
        resolve('1')
      })
      return p;
    }
    function promise2 () {
      return new Promise((resolve, reject) => {
        reject('error')
      })
    }
    promise1()
      .then(res => console.log(res))
      .catch(err => console.log(err))
      .then(() => console.log('finally1'))
    ​
    promise2()
      .then(res => console.log(res))
      .catch(err => console.log(err))
      .then(() => console.log('finally2'))
    

Promise中的all和race

  • 总结

    • Promise.all()的作用是接收一组异步任务,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调。
    • .race()的作用也是接收一组异步任务,然后并行执行异步任务,只保留取第一个执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。
    • Promise.all().then()结果中数组的顺序和Promise.all()接收到的数组顺序一致。
    • all和race传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被then的第二个参数或者后面的catch捕获;但并不会影响数组中其它的异步任务的执行。
  • 题目一:

    function runAsync (x) {
        const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
        return p
    }
    Promise.all([runAsync(1), runAsync(2), runAsync(3)])
      .then(res => console.log(res))
    

    执行结果:

    1
    2
    3
    [1, 2, 3]
    

    有了all,就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据。

    .all()后面的.then()里的回调函数接收的就是所有异步操作的结果。

    而且这个结果中数组的顺序和Promise.all()接收到的数组顺序一致。

    有一个场景是很适合用这个的,一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。

  • 题目二

    新增了一个runReject函数,它用来在1000 * x秒后reject一个错误。

    同时.catch()函数能够捕获到.all()里最先的那个异常,并且只执行一次。

    function runAsync (x) {
      const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
      return p
    }
    function runReject (x) {
      const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
      return p
    }
    Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
      .then(res => console.log(res))
      .catch(err => console.log(err))
    

    执行结果:

    // 1s后输出
    1
    3
    // 2s后输出
    2
    Error: 2
    // 4s后输出
    4
    
  • 题目三

    改造一下题目一:

    function runAsync (x) {
      const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
      return p
    }
    Promise.race([runAsync(1), runAsync(2), runAsync(3)])
      .then(res => console.log('result: ', res))
    

    使用.race()方法,它只会获取最先执行完成的那个结果,其它的异步任务虽然也会继续进行下去,不过race已经不管那些任务的结果了。

    race的使用场景还是很多的,比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作。

  • 题目四

    改造下题目4.2:

    function runAsync(x) {
      const p = new Promise(r =>
        setTimeout(() => r(x, console.log(x)), 1000)
      );
      return p;
    }
    function runReject(x) {
      const p = new Promise((res, rej) =>
        setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)
      );
      return p;
    }
    Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)])
      .then(res => console.log("result: ", res))
      .catch(err => console.log(err));
    

    执行结果:

    0
    'Error: 0'
    1
    2
    3
    

async/await

  • 题目一:

    async function async1() {
      console.log("async1 start");
      await async2();
      console.log("async1 end");
    }
    async function async2() {
      console.log("async2");
    }
    async1();
    console.log('start')
    

    执行结果

    'async1 start'
    'async2'
    'start'
    'async1 end'
    

    过程分析:

    • 首先一进来是创建了两个函数的,我们先不看函数的创建位置,而是看它的调用位置
    • 发现async1函数被调用了,然后去看看调用的内容
    • 执行函数中的同步代码async1 start,之后碰到了await,它会阻塞async1后面代码的执行,因此会先去执行async2中的同步代码async2,然后跳出async1
    • 跳出async1函数后,执行同步代码start
    • 在一轮宏任务全部执行完之后,再来执行刚刚await后面的内容async1 end

    在这里,可以理解为「紧跟着await后面的语句相当于放到了new Promise中,下一行及之后的语句相当于放在Promise.then中」。

    • 题目二

      async function async1() {
        console.log("async1 start");
        await async2();
        console.log("async1 end");
      }
      async function async2() {
        setTimeout(() => {
          console.log('timer')
        }, 0)
        console.log("async2");
      }
      async1();
      console.log("start")
      

      执行结果

      'async1 start'
      'async2'
      'start'
      'async1 end'
      'timer'
      

      定时器始终还是最后执行的,它被放到下一条宏任务的延迟队列中。

    • 题目三

      async function async1() {
        console.log("async1 start");
        await async2();
        console.log("async1 end");
        setTimeout(() => {
          console.log('timer1')
        }, 0)
      }
      async function async2() {
        setTimeout(() => {
          console.log('timer2')
        }, 0)
        console.log("async2");
      }
      async1();
      setTimeout(() => {
        console.log('timer3')
      }, 0)
      console.log("start")
      

      执行结果:

      'async1 start'
      'async2'
      'start'
      'async1 end'
      'timer2'
      'timer3'
      'timer1'
      

      定时器谁先执行,只需要关注谁先被调用的以及延迟时间是多少,这道题中延迟时间都是0,所以只要关注谁先被调用。

    • 题目四

      async function async1 () {
        console.log('async1 start');
        await new Promise(resolve => {
          console.log('promise1')
        })
        console.log('async1 success');
        return 'async1 end'
      }
      console.log('srcipt start')
      async1().then(res => console.log(res))
      console.log('srcipt end')
      

      async1await后面的Promise是没有返回值的,也就是它的状态始终是pending状态,因此相当于一直在awaitawaitawait却始终没有响应。

      await之后的内容是不会执行的,也包括async1后面的 .then

      执行结果:

      'script start'
      'async1 start'
      'promise1'
      'script end'
      
    • 题目五

      给上题中的Promise加上resolve

      async function async1 () {
        console.log('async1 start');
        await new Promise(resolve => {
          console.log('promise1')
          resolve('promise1 resolve')
        }).then(res => console.log(res))
        console.log('async1 success');
        return 'async1 end'
      }
      console.log('srcipt start')
      async1().then(res => console.log(res))
      console.log('srcipt end')
      

      现在Promise有了返回值了,因此await后面的内容将会被执行:

      'script start'
      'async1 start'
      'promise1'
      'script end'
      'promise1 resolve'
      'async1 success'
      'async1 end'
      
    • 题目六

      async function async1 () {
        console.log('async1 start');
        await new Promise(resolve => {
          console.log('promise1')
          resolve('promise resolve')
        })
        console.log('async1 success');
        return 'async1 end'
      }
      console.log('srcipt start')
      async1().then(res => {
        console.log(res)
      })
      new Promise(resolve => {
        console.log('promise2')
        setTimeout(() => {
          console.log('timer')
        })
      })
      

      async1中的new Promise它的resovle的值和async1().then()里的值是没有关系的,可能看到resovle('promise resolve')就会误以为是async1().then()中的返回值。

      执行结果:

      'script start'
      'async1 start'
      'promise1'
      'promise2'
      'async1 success'
      'async1 end'
      'timer'
      
    • 题目七

      async function async1() {
        console.log("async1 start");
        await async2();
        console.log("async1 end");
      }
      ​
      async function async2() {
        console.log("async2");
      }
      ​
      console.log("script start");
      ​
      setTimeout(function() {
        console.log("setTimeout");
      }, 0);
      ​
      async1();
      ​
      new Promise(function(resolve) {
        console.log("promise1");
        resolve();
      }).then(function() {
        console.log("promise2");
      });
      console.log('script end')
      

      执行结果:

      'script start'
      'async1 start'
      'async2'
      'promise1'
      'script end'
      'async1 end'
      'promise2'
      'setTimeout'
      
    • 题目八

      async function testSometing() {
        console.log("执行testSometing");
        return "testSometing";
      }
      ​
      async function testAsync() {
        console.log("执行testAsync");
        return Promise.resolve("hello async");
      }
      ​
      async function test() {
        console.log("test start...");
        const v1 = await testSometing();
        console.log(v1);
        const v2 = await testAsync();
        console.log(v2);
        console.log(v1, v2);
      }
      ​
      test();
      ​
      var promise = new Promise(resolve => {
        console.log("promise start...");
        resolve("promise");
      });
      promise.then(val => console.log(val));
      ​
      console.log("test end...");
      

      执行结果:

      'test start...'
      '执行testSometing'
      'promise start...'
      'test end...'
      'testSometing'
      '执行testAsync'
      'promise'
      'hello async'
      'testSometing' 'hello async'
      

async处理错误

  • 题目一

    async中,如果 await后面的内容是一个异常或者错误的话,会怎样呢?

    async function async1 () {
      await async2();
      console.log('async1');
      return 'async1 success'
    }
    async function async2 () {
      return new Promise((resolve, reject) => {
        console.log('async2')
        reject('error')
      })
    }
    async1().then(res => console.log(res))
    

    这道题中,await后面跟着的是一个状态为rejectedpromise

    如果在async函数中抛出了错误,则终止错误结果,不会继续向下执行。

    所以答案为:

    'async2'
    Uncaught (in promise) error
    

    如果改为throw new Error也是一样的:

    async function async1 () {
      console.log('async1');
      throw new Error('error!!!')
      return 'async1 success'
    }
    async1().then(res => console.log(res))
    

    结果为:

    'async1'
    Uncaught (in promise) Error: error!!!
    
  • 题目二

    如果想要使得错误的地方不影响async函数后续的执行的话,可以使用try catch

    async function async1 () {
      try {
        await Promise.reject('error!!!')
      } catch(e) {
        console.log(e)
      }
      console.log('async1');
      return Promise.resolve('async1 success')
    }
    async1().then(res => console.log(res))
    console.log('script start')
    

    执行结果:

    'script start'
    'error!!!'
    'async1'
    'async1 success'
    

    或者你可以直接在Promise.reject后面跟着一个catch()方法:

    async function async1 () {
      // try {
      //   await Promise.reject('error!!!')
      // } catch(e) {
      //   console.log(e)
      // }
      await Promise.reject('error!!!')
        .catch(e => console.log(e))
      console.log('async1');
      return Promise.resolve('async1 success')
    }
    async1().then(res => console.log(res))
    console.log('script start')
    

    运行结果是一样的。

综合题

  • 题目一

    const first = () => (new Promise((resolve, reject) => {
        console.log(3);
        let p = new Promise((resolve, reject) => {
            console.log(7);
            setTimeout(() => {
                console.log(5);
                resolve(6);
                console.log(p)
            }, 0)
            resolve(1);
        });
        resolve(2);
        p.then((arg) => {
            console.log(arg);
        });
    }));
    first().then((arg) => {
        console.log(arg);
    });
    console.log(4);
    

    过程分析:

    • 第一段代码定义的是一个函数,所以我们得看看它是在哪执行的,发现它在4之前,所以可以来看看first函数里面的内容了。(这一步有点类似于题目1.5)
    • 函数first返回的是一个new Promise(),因此先执行里面的同步代码3
    • 接着又遇到了一个new Promise(),直接执行里面的同步代码7
    • 执行完7之后,在p中,遇到了一个定时器,先将它放到下一个宏任务队列里不管它,接着向下走
    • 碰到了resolve(1),这里就把p的状态改为了resolved,且返回值为1,不过这里也先不执行
    • 跳出p,碰到了resolve(2),这里的resolve(2),表示的是把first函数返回的那个Promise的状态改了,也先不管它。
    • 然后碰到了p.then,将它加入本次循环的微任务列表,等待执行
    • 跳出first函数,遇到了first().then(),将它加入本次循环的微任务列表(p.then的后面执行)
    • 然后执行同步代码4
    • 本轮的同步代码全部执行完毕,查找微任务列表,发现p.thenfirst().then(),依次执行,打印出1和2
    • 本轮任务执行完毕了,发现还有一个定时器没有跑完,接着执行这个定时器里的内容,执行同步代码5
    • 然后又遇到了一个resolve(6),它是放在p里的,但是p的状态在之前已经发生过改变了,因此这里就不会再改变,也就是说resolve(6)相当于没任何用处,因此打印出来的pPromise{<resolved>: 1}。(这一步类似于题目3.1)

    执行结果:

    3
    7
    4
    1
    2
    5
    Promise{<resolved>: 1}
    
  • 题目二

    const async1 = async () => {
      console.log('async1');
      setTimeout(() => {
        console.log('timer1')
      }, 2000)
      await new Promise(resolve => {
        console.log('promise1')
      })
      console.log('async1 end')
      return 'async1 success'
    } 
    console.log('script start');
    async1().then(res => console.log(res));
    console.log('script end');
    Promise.resolve(1)
      .then(2)
      .then(Promise.resolve(3))
      .catch(4)
      .then(res => console.log(res))
    setTimeout(() => {
      console.log('timer2')
    }, 1000)
    

    注意的知识点:

    • async函数中awaitnew Promise要是没有返回值的话则不执行后面的内容
    • .then函数中的参数期待的是函数,如果不是函数的话会发生透传
    • 注意定时器的延迟时间

    执行结果为:

    'script start'
    'async1'
    'promise1'
    'script end'
    1
    'timer2'
    'timer1'
    
  • 题目三

    const p1 = new Promise((resolve) => {
      setTimeout(() => {
        resolve('resolve3');
        console.log('timer1')
      }, 0)
      resolve('resovle1');
      resolve('resolve2');
    }).then(res => {
      console.log(res)
      setTimeout(() => {
        console.log(p1)
      }, 1000)
    }).finally(res => {
      console.log('finally', res)
    })
    

    注意的知识点:

    • Promise的状态一旦改变就无法改变(类似题目3.5)
    • finally不管Promise的状态是resolved还是rejected都会执行,且它的回调函数是接收不到Promise的结果的,所以finally()中的res是一个迷惑项(类似3.10)。
    • 最后一个定时器打印出的p1其实是.finally的返回值,我们知道.finally的返回值如果在没有抛出错误的情况下默认会是上一个Promise的返回值(3.10中也有提到), 而这道题中.finally上一个Promise.then(),但是这个.then()并没有返回值,所以p1打印出来的Promise的值会是undefined,如果你在定时器的下面加上一个return 1,则值就会变成1(感谢掘友JS丛中过的指出)。

    执行结果:

    'resolve1'
    'finally' undefined
    'timer1'
    Promise{<resolved>: undefined}
    

几道大厂面试题

  • 使用Promise实现每隔1秒输出1,2,3

    这道题比较简单的一种做法是可以用Promise配合着reduce不停的在promise后面叠加.then

    let arr = [1, 2, 3];
    arr.reduce((p, x) => {
      return p.then(() => {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve(console.log(x));
          }, 1000);
        });
      });
    }, Promise.resolve());
    
  • 使用Promise实现红绿灯交替重复亮

    红灯3秒亮一次,黄灯2秒亮一次,绿灯1秒亮一次;如何让三个灯不断交替重复亮灯?(用Promise实现)三个亮灯函数已经存在:

    function red() {
        console.log('red');
    }
    function green() {
        console.log('green');
    }
    function yellow() {
        console.log('yellow');
    }
    

    答案:

    function red() {
      console.log("red");
    }
    function green() {
      console.log("green");
    }
    function yellow() {
      console.log("yellow");
    }
    ​
    const light = (timer, cb) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          cb();
          resolve();
        }, timer);
      });
    };
    ​
    const step = () => {
      Promise.resolve()
        .then(() => {
          return light(3000, red);
        })
        .then(() => {
          return light(2000, green);
        })
        .then(() => {
          return light(1000, yellow);
        })
        .then(() => {
          return step();
        });
    };
    ​
    step();
    
  • 实现mergePromise函数

    实现mergePromise函数,把传进去的数组按顺序先后执行,并且把返回的数据先后放到数组data中。

    const time = (timer) => {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve()
        }, timer)
      })
    }
    const ajax1 = () => time(2000).then(() => {
      console.log(1);
      return 1
    })
    const ajax2 = () => time(1000).then(() => {
      console.log(2);
      return 2
    })
    const ajax3 = () => time(1000).then(() => {
      console.log(3);
      return 3
    })
    ​
    function mergePromise () {
      // 在这里写代码
    }
    ​
    mergePromise([ajax1, ajax2, ajax3]).then(data => {
      console.log("done");
      console.log(data); // data 为 [1, 2, 3]
    });
    ​
    // 要求分别输出
    // 1
    // 2
    // 3
    // done
    // [1, 2, 3]
    

    这道题有点类似于Promise.all(),不过.all()不需要管执行顺序,只需要并发执行就行了。但是这里需要等上一个执行完毕之后才能执行下一个。

    解题思路:

    • 定义一个数组data用于保存所有异步操作的结果
    • 初始化一个const onePromise = Promise.resolve(),然后循环遍历数组,在promise后面添加执行ajax任务,同时要将添加的结果重新赋值到promise上。

    答案

    function mergePromise(promises) {
      // 在这里写代码
      const data = [];
      let onePromise = Promise.resolve();
      promises.forEach((promise) => {
        onePromise = onePromise.then(promise).then((res) => {
          data.push(res);
          return data;
        });
      });
      return onePromise;
    }
    
  • 封装一个异步加载图片的方法

    只需要在图片的onload函数中,使用resolve返回一下就可以了。

    function loadImg(url) {
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = function() {
          console.log("一张图片加载完成");
          resolve(img);
        };
        img.onerror = function() {
          reject(new Error('Could not load image at' + url));
        };
        img.src = url;
      });
    
  • 限制异步操作的并发个数并尽可能快的完成全部

    有8个图片资源的url,已经存储在数组urls中。

    urls类似于['image1.png', 'image2.png', ....]

    而且已经有一个函数function loadImg,输入一个url链接,返回一个Promise,该Promise在图片下载完成的时候resolve,下载失败则reject

    但有一个要求,任何时刻同时下载的链接数量不可以超过3个

    请写一段代码实现这个需求,要求尽可能快速地将所有图片下载完成。

    var urls = [
      "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting1.png",
      "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting2.png",
      "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting3.png",
      "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting4.png",
      "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting5.png",
      "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn6.png",
      "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn7.png",
      "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn8.png",
    ];
    function loadImg(url) {
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = function() {
          console.log("一张图片加载完成");
          resolve(img);
        };
        img.onerror = function() {
          reject(new Error('Could not load image at' + url));
        };
        img.src = url;
      });
    

    题目的要求是保证每次并发请求的数量为3,那么我们可以先请求urls中的前面三个(下标为0,1,2),并且请求的时候使用Promise.race()来同时请求,三个中有一个先完成了(例如下标为1的图片),我们就把这个当前数组中已经完成的那一项(第1项)换成还没有请求的那一项(urls中下标为3)。

    直到urls已经遍历完了,然后将最后三个没有完成的请求(也就是状态没有改变的Promise)用Promise.all()来加载它们。

    流程图:

    img

    实现代码:

    function limitLoad(urls, handler, limit) {
      let sequence = [].concat(urls); // 复制urls
      // 这一步是为了初始化 promises 这个"容器"
      let promises = sequence.splice(0, limit).map((url, index) => {
        return handler(url).then(() => {
          // 返回下标是为了知道数组中是哪一项最先完成
          return index;
        });
      });
      // 注意这里要将整个变量过程返回,这样得到的就是一个Promise,可以在外面链式调用
      return sequence
        .reduce((pCollect, url) => {
          return pCollect
            .then(() => {
              return Promise.race(promises); // 返回已经完成的下标
            })
            .then(fastestIndex => { // 获取到已经完成的下标
              // 将"容器"内已经完成的那一项替换
              promises[fastestIndex] = handler(url).then(
                () => {
                  return fastestIndex; // 要继续将这个下标返回,以便下一次变量
                }
              );
            })
            .catch(err => {
              console.error(err);
            });
        }, Promise.resolve()) // 初始化传入
        .then(() => { // 最后三个用.all来调用
          return Promise.all(promises);
        });
    }
    limitLoad(urls, loadImg, 3)
      .then(res => {
        console.log("图片全部加载完毕");
        console.log(res);
      })
      .catch(err => {
        console.error(err);
      });