Promise 相关知识

400 阅读9分钟

1. Promise

Promise对象有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。一旦状态改变,就不会再变。

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

如果不设置回调函数,Promise内部抛出的错误,不会反应到外部

1.1 Promise 的状态如何改变

    const promise = new Promise(function(resolve, reject) {
      // ... some code

      if (/* 异步操作成功 */){
        resolve(value);
      } else {
        reject(error);
      }
    });

new Promise()时,会立即执行传进来的exector函数,并向exector函数传递两个参数,一般命名是resolvereject它们是两个函数,由JavaScript引擎提供,不用自己部署。

  1. resolve:将Promise对象的状态从pending变为resolved,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
  2. reject:将Promise对象的状态从pending变为rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去;
  3. 如果exector函数执行报错,则会将Promise对象的状态从pending变为rejected,并将报错信息传递出去;
  4. A的状态由B决定。 对于第4点,调用resolve/reject函数时带有参数,那么它们的参数会被传递给回调函数。resolve/reject函数的参数除了正常的值以外,还可能是另一个Promise实例,比如像下面这样。
    const p1 = new Promise(function (resolve, reject) {
      setTimeout(() => reject(new Error('fail')), 3000)
    })

    const p2 = new Promise(function (resolve, reject) {
      setTimeout(() => resolve(p1), 1000)
    })

    p2
      .then(result => console.log(result))
      .catch(error => console.log(error))  // Error: fail

上面代码中,p1是一个Promise,3秒之后变为rejectedp2的状态在1秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了2秒,p1变为rejected,导致触发catch方法指定的回调函数。

另外,不同的调用会导致不同的结果:

  const p1 = new Promise(function (resolve, reject) {
>     setTimeout(() => resolve(999), 200)
  })

  const p2 = new Promise(function (resolve, reject) {
      setTimeout(() => resolve(p1), 10)
  })

  p2
    .then(result => console.log(result, 'resolve')) // 999 "resolve"
    .catch(error => console.log(error, 'reject'))
  const p1 = new Promise(function (resolve, reject) {
      setTimeout(() => resolve(999), 200)
  })

  const p2 = new Promise(function (resolve, reject) {
>     setTimeout(() => reject(p1), 10)
  })

  p2
    .then(result => console.log(result, 'resolve'))
    .catch(error => console.log(error, 'reject'))
/* 输出
  Promise {<pending>}
    __proto__: Promise
      [[PromiseState]]: "fulfilled"
      [[PromiseResult]]: 999
  "reject"
**/
  const p1 = new Promise(function (resolve, reject) {
>     setTimeout(() => reject(999), 200)
  })

  const p2 = new Promise(function (resolve, reject) {
      setTimeout(() => reject(p1), 10)
  })

  p2
    .then(result => console.log(result, 'resolve'))
    .catch(error => console.log(error, 'reject'))
/**
  Promise {<pending>}
    __proto__: Promise
      [[PromiseState]]: "rejected"
      [[PromiseResult]]: 999
  "reject"
**/

由此得出结论:

  • p2resolve(p1)来改变状态,则p2的状态由p1决定,若p1状态为pending,则会等待p1状态改变之后,p2的状态才会改变。并且传递的值为p1resolve/reject的参数。
  • p2reject(p1)来改变状态,那么p2的状态由自己决定,并且传递的值为p1对象。

1.2 Promise.prototype.then

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

那么新Promise实例的状态和值,由谁来决定?看下面这个例子:

  let p1 = new Promise((resolve, reject) => {
    reject(-100);
  });

  let p2 = p1.then(result => {
    console.log(`成功:${result}`);
    return 200;
  }, reason => {
    console.log(`失败:${reason}`); // 失败:-100
    return -200;
  });

  let p3 = p2.then(result => {
    console.log(`成功:${result}`); // 成功:-200
    throw new Error('xxxx')
  }, reason => {
    console.log(`失败:${reason}`);
  });

  p3.then(result => {
    console.log(`成功:${result}`);
  }, reason => {
    console.log(`失败:${reason}`); //失败:Error: xxxx
  });

不论p1fulfilled或是rejected,我们只看p1.then执行是否报错;如果报错则p2的状态是失败rejected,值是报错信息;如果不报错则p2的状态是成功fulfilled,值是函数的返回值。

此概念可以借助try/catch的异常处理机制来理解,Promise.prototype.then(resolution, rejection)等价于Promise.prototype.then().catch(),这其实就是异常的处理过程,异常一层层向上抛出(Promise是向后冒)。

  • p1状态为fulfilled,则执行then函数,then执行完,返回的新实例状态为fulfilled
  • p1状态为rejected,则执行catch函数,也就是异常处理函数,异常在catch中被处理掉,就没有异常了,故返回的新实例状态也是fulfilled
  • 若在执行then/catch函数时再次发生异常,则异常向上抛出,返回的新实例状态为rejected若该新实例有then/catch处理函数,则按上面的规则处理,若没有,则错误不会传递到外层代码。
  • 《ES6》:跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。 所以无论p1状态如何,只要在执行then/catch的过程中没有代码错误,那么返回的新实例状态为fulfiled

新实例的状态还有另一种情况:执行不报错,但是返回的值是另一个Promise实例,这时新实例的状态会由该Promise对象的状态决定。 值是该Promise的值。

下面有几个例子用来帮助理解:

  Promise.reject(0).then(result => {    // 1
    console.log(`成功:${result}`);
    return 1;
  }).then(result => {                   // 2
    console.log(`成功:${result}`);
    return 2;
  }).then(result => {                   // 3
    console.log(`成功:${result}`);
    return 3;
  }, reason => {                        // 4
    console.log(`失败:${reason}`); // 失败:0
  });
  /**
   * try/catch:
   *    Promise.reject(0):发生异常,1与2只有一个参数,所以捕获不到异常,异常4中被处理
   * js:
   *    Promise.reject(0):返回rejected状态的Promise对象,1与2只有一个参数所以没有执行,则4执行
  */

  Promise.resolve(100).then(result => {   // 1
    console.log(`成功:${result}`); // 成功:100
    throw 'xxx';
  }).then(result => {                     // 2
    console.log(`成功:${result}`);
    return 2;
  }).then(result => {                     // 3
    console.log(`成功:${result}`);
    return 3;
  }, reason => {                          // 4
    console.log(`失败:${reason}`); // 失败:xxx
  });
  /**
   * try/catch:
   *    Promise.resolve(100):1在执行时抛出异常,被4捕获到
   * js:
   *    Promise.resolve(100):返回fulfiled状态的Promise对象,1执行报错,返回新实例状态为rejected,则4执行
  */

  Promise.resolve(100).then(result => {     // 1
    console.log(`成功:${result}`); // 成功:100
    return 1;
  }).then(result => {                       // 2
    console.log(`成功:${result}`); // 成功:1
    return Promise.reject('NO');
  }).catch(reason => {                      // 3
    console.log(`失败:${reason}`); // 失败:NO
  });
  /**
   * Promise.resolve(100):12正常执行,2返回rejected状态的Promise对象,故2返回的新实例状态为rejected,则3执行
  */

1.3 Promise.prototype.catch

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

1.4 Promise.prototype.finally

finally()方法用于指定不管Promise对象最后状态如何,都会执行的操作。

finally本质上是then方法的特例:

    promise
    .finally(() => {
      // 语句
    });

    // 等同于
    promise
    .then(
      result => {
        // 语句
        return result;
      },
      error => {
        // 语句
        throw error;
      }
    );
Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

从上面的实现还可以看到,finally方法总是会返回原来的值。

1.5 Promise.all

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

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

p的状态由p1p2p3决定,分成两种情况。

  1. 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。
  2. 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
  3. 如果作为参数的Promise实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。
  let p1 = Promise.resolve(1);
  let p2 = Promise.reject('xxx').catch(err => err);
  let p3 = Promise.resolve(3);

  const p = Promise.all([p1, p2, p3]);
  p.then(res => {
    console.log(res);  // [1, "xxx", 3]
  }).catch(err => {
    console.log(err);
  })

1.6 Promise.race

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

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

只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的回调函数。

  let p1 = function () {
    return new Promise((resolve, reject) => {
      setTimeout(reject, 100, 1);
    })
  }();
  let p2 = function () {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, 200, 2);
    })
  }();
  let p3 = function () {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, 300, 3);
    })
  }();

  const p = Promise.race([p1, p2, p3]);
  p.then(res => {
    console.log(res);  // 1
  }).catch(err => {
    console.log(err);
  })

1.7 Promise.allSettled

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

该方法返回的新的Promise实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise的监听函数接收到的参数是一个数组,数组中的每个成员对应一个传入Promise.allSettled()Promise实例。

  const p1 = function () {
    return new Promise((resolve, reject) => {
      setTimeout(reject, 100, 1);
    })
  }();
  const p2 = function () {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, 200, 2);
    })
  }();

  const p = Promise.allSettled([p1, p2]);
  p.then(res => {
    console.log(res);  // [ {status: "rejected", reason: 1}, {status: "fulfilled", value: 2}]
  }).catch(err => {
    console.log(err);
  })

注意res的格式,每个对象都有status属性,status: 'fulfilled' | 'rejected'fulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值。

2. Symbol / Iterator / Generator

2.1 Symbol

ES5的对象属性名都是字符串,这容易造成属性名的冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是ES6引入Symbol的原因。

ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

Symbol值通过Symbol函数生成。Symbol函数前不能使用new命令,否则会报错。 这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

Symbol函数的参数只是表示对当前Symbol值的描述,因此相同参数的Symbol函数的返回值是不相等的。

    // 没有参数的情况
    let s1 = Symbol();
    let s2 = Symbol();

    s1 === s2 // false

    // 有参数的情况
    let s1 = Symbol('foo');
    let s2 = Symbol('foo');

    s1 === s2 // false

Symbol值不能与其他类型的值进行运算,Symbol只可隐式转换为布尔值,显式转为字符串。否则会报错。

    let sym = Symbol('My symbol');

    "your symbol is " + sym
    // TypeError: can't convert symbol to string
    `your symbol is ${sym}`
    // TypeError: can't convert symbol to string

    String(sym) // 'Symbol(My symbol)'
    sym.toString() // 'Symbol(My symbol)'
    let sym = Symbol();
    Boolean(sym) // true
    !sym  // false

    if (sym) {
      // ...
    }

    Number(sym) // TypeError
    sym + 2 // TypeError

Symbol.prototype.description

Symbol.prototype.description用于返回创建Symbol值时的描述。

    const sym = Symbol('foo');
    sym.description // "foo"

Symbol.for(),Symbol.keyFor()

  • Symbol.for():它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建一个以该字符串为名称的Symbol值,并将其注册到全局(全局环境,不管有没有在全局环境运行)。
  • Symbol.keyFor():返回一个已登记的Symbol类型值的key
    Symbol.for("bar") === Symbol.for("bar") // true
    Symbol.for("bar") === Symbol("bar") // false
    Symbol("bar") === Symbol("bar") // false

Symbol.iterator

对象的Symbol.iterator属性,指向该对象的默认遍历器方法。

2.2 Iterator

遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator的遍历过程是这样的。

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
  4. 不断调用指针对象的next方法,直到它指向数据结构的结束位置。 每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

下面是一个模拟next方法返回值的例子。

  var it = makeIterator(['a', 'b']);

  it.next() // { value: "a", done: false }
  it.next() // { value: "b", done: false }
  it.next() // { value: undefined, done: true }

  function makeIterator(array) {
    var nextIndex = 0;
    return {
      next: function() {
        return nextIndex < array.length ?
          {value: array[nextIndex++], done: false} :
          {value: undefined, done: true};
      }
    };
  }

Iterator接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环(详见下文)。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找Iterator接口。

默认的Iterator接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。

原生具备Iterator接口的数据结构如下:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的arguments对象
  • NodeList对象

默认调用Iterator接口的场合:

  • 解构赋值
  • 扩展运算符
  • yield*
  • for...of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
  • Promise.all()
  • Promise.race()

2.3 Generator

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

Generator函数的执行过程

如下示例:

  function* helloWorldGenerator() {
    yield 'hello';
    yield 'world';
    return 'ending';
  }

  var hw = helloWorldGenerator();
  hw.next() // { value: 'hello', done: false }
  hw.next() // { value: 'world', done: false }
  hw.next() // { value: 'ending', done: true }
  hw.next() // { value: undefined, done: true }

上面代码一共调用了四次next方法。

  • 第一次调用,Generator函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hellodone属性的值false,表示遍历还没有结束。
  • 第二次调用,Generator函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值worlddone属性的值false,表示遍历还没有结束。
  • 第三次调用,Generator函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。
  • 第四次调用,此时Generator函数已经运行完毕,next方法返回对象的value属性为undefineddone属性为true。以后再调用next方法,返回的都是这个值。