Promise的基础知识

Promist的生命周期

每个Promise都会经历一个短暂的生命周期:先是处于进行中(pending)的状态,此时操作尚未完成,所以它也是未处理(unsettled)的;一旦异步操作执行结束,Promise则变为已处理(settled)的状态。在之前的示例中,当readFile()函数返回Promise时它变为pending状态,操作结束后,Promise可能会进入到以下两个状态中的其中一个:
· Fulfilled Promise异步操作成功完成。
· Rejected 由于程序错误或一些其他原因,Promise异步操作未能成功完成。

promise.then()方法

所有Promise都有then()方法,它接受两个参数:
第一个是当Promise的状态变为fulfilled时要调用的函数,与异步操作相关的附加数据都会传递给这个完成函数(fulfillment function);
第二个是当Promise的状态变为rejected时要调用的函数,其与完成时调用的函数类似,所有与失败状态相关的附加数据都会传递给这个拒绝函 数(rejection function)。

  1. let promise = readFile("example.txt");
  2. promise.then(function (contents) {
  3. // 完成
  4. console.log(contents);
  5. }, function (err) {
  6. // 拒绝
  7. console.error(err.message);
  8. });
  9. promise.then(function (contents) {
  10. // 完成
  11. console.log(contents);
  12. });
  13. promise.then(null, function (err) {
  14. // 拒绝
  15. console.error(err.message);
  16. })

上面这3次then()调用操作的是同一个Promise。第一个同时监听了执行完成和执行被拒;第二个只监听了执行完成,错误时不报告;第三个只监听了执行被拒,成功时不报告。

promise.catch()

  1. promise.catch(function (err) {
  2. // 拒绝
  3. console.error(err.message);
  4. });
  5. // 与以下调用相同
  6. promise.then(null, function (err) {
  7. // 拒绝
  8. console.log(err.message);
  9. })

创造未完成的Promise()

用Promise构造函数可以创建新的Promise,构造函数只接受一个参数:包含初始化Promise代码的执行器(executor)函数。执行器接受两个参数,分别是resolve()函数和reject()函数。执行器成功完成时调用resolve()函数,反之,失败时则调用reject()函数。

调用resolve()后会触发一个异步操作,传入then()和catch()方法的函数会被添加到任务队列中并异步执行(不会立即执行)。请看这个示例:

  1. let promise = new Promise(function (resolve, reject) {
  2. console.log("Promise");
  3. resolve();
  4. });
  5. promise.then(function () {
  6. console.log("Resolve");
  7. });
  8. console.log("Hi");

请注意,即使在代码中then()调用位于console.log(“Hi!”)之前,但其与执行器不同,它并没有立即执行。这是因为,完成处理程序和拒绝处理程序总是在执行器完成后被添加到任务队列的末尾。

创建已处理的Promise

使用Promise.resolve()

Promise.resolve()方法只接受一个参数并返回一个完成态的Promise,也就是说不会有任务编排的过程,而且需要向Promise添加一至多个完成处理程序来获取值。例如:

  1. let promise = Promise.resolve(42);
  2. promise.then(function (value) {
  3. console.log(value); //42
  4. })

使用Promise.reject()

也可以通过Promise.reject()方法来创建已拒绝Promise,它与Promise.resolve()很像,唯一的区别是创建出来的是拒绝态的Promise,例如:

  1. let promise = Promise.reject(42);
  2. promise.then(function (value) {
  3. console.log(value); //42
  4. })

非Promise的Thenable对象

Promise.resolve()方法和Promise.reject()方法都可以接受非Promise的Thenable对象作为参数。如果传入一个非Promise的Thenable对象,则这些方法会创建一个新的Promise,并在then()函数中被调用。

拥有then()方法并且接受resolve和reject这两个参数的普通对象就是非Promise的Thenable对象,例如:

  1. let thenable = {
  2. then: function (resolve, reject) {
  3. resolve(42);
  4. }
  5. }

在此示例中,Thenable对象和Promise之间只有then()方法这一个相似之处,可以调用Promise.resolve()方法将Thenable对象转换成一个已完成Promise:

  1. let thenable = {
  2. then: function (resolve, reject) {
  3. resolve(42);
  4. }
  5. };
  6. let p1 = Promise.resolve(thenable);
  7. p1.then(function (value) {
  8. console.log(value); //42
  9. });

可以使用与Promise.resolve()相同的过程创建基于Thenable对象的已拒绝Promise:

  1. let thenable = {
  2. then: function (resolve, reject) {
  3. resolve(42);
  4. }
  5. };
  6. let p1 = Promise.resolve(thenable);
  7. p1.catch(function (value) {
  8. console.log(value); //42
  9. });

执行器错误

如果执行器内部抛出一个错误,则Promise的拒绝处理程序就会被调用,例如:

  1. let promise = new Promise(function (resolve, reject) {
  2. throw new Error("err");
  3. });
  4. promise.catch(function (error) {
  5. console.log(error.message);
  6. })

等价于

  1. let promise = new Promise(function (resolve, reject) {
  2. try {
  3. throw new Error("err");
  4. } catch (ex) {
  5. reject(ex);
  6. }
  7. });
  8. promise.catch(function (error) {
  9. console.log(error.message); // err
  10. })

全局的Promise拒绝处理

有关Promise的其中一个最具争议的问题是,如果在没有拒绝处理程序的情况下拒绝一个Promise,那么不会提示失败信息,这是JavaScript语言中唯一一处没有强制报错的地方,一些人认为这是标准中最大的缺陷。 Promise的特性决定了很难检测一个Promise是否被处理过,例如:

浏览器也是通过触发两个事件来识别未处理的拒绝的,虽然这些事件是在window对象上触发的,但实际上与Node.js中的完全等效。
· unhandledrejection 在一个事件循环中,当Promise被拒绝,并且没有提供拒绝处理程序时被调用。
· rejectionhandled 在一个事件循环后,当Promise被拒绝,并且没有提供拒绝处理程序时被调用。
在Node.js实现中,事件处理程序接受多个独立参数;而在浏览器中,事件处理程序接受一个有以下属性的事件对象作为参数:
· type 事件名称(”unhandledrejection”或”rejectionhandled”)
· promise 被拒绝的Promise对象
· reason 来自Promise的拒绝值
浏览器实现中的另一处不同是,在两个事件中都可以使用拒绝值(reason),例如:

  1. let rejected;
  2. window.onunhandledrejection = function (event) {
  3. console.log(event.type);
  4. console.log(event.reason.message);
  5. console.log(rejected === event.promise);
  6. };
  7. window.onrejectionhandled = function (event) {
  8. console.log(event.type);
  9. console.log(event.reason.message);
  10. console.log(rejected === event.promise);
  11. }
  12. rejected = promise.rejected(new Error("err"));

这段代码用DOM 0级记法的onunhandledrejection和onrejectionhandled给两个事件处理程序赋值,如果你愿意的话也可以使用addEventListener(“unhandledrejection”)和addEventListener(“rejectionhandled”),每个事件处理程序接受一个含有被拒绝Promise信息的事件对象,该对象的属性type、promise和reason在这两个事件处理程序中均可使用。
在浏览器中,跟踪未处理拒绝也与Node.js中的非常相似

  1. let possiblyUnhandledRejections = new Map();
  2. // 如果一个拒绝没有背处理,则将它添加到Map集合中
  3. window.onunhandledrejection = function (event) {
  4. possiblyUnhandledRejections.set(event.promise, event.reason);
  5. };
  6. window.onrejectionhandled = function (event) {
  7. possiblyUnhandledRejections.delete(event.promise);
  8. }
  9. setInterval(function () {
  10. possiblyUnhandledRejections.forEach(function (reason, promise) {
  11. console.log(reason.message ? reason.message : reason);
  12. //做一些什么来处理这些拒绝
  13. handleRejection(promise, reason);
  14. });
  15. possiblyUnhandledRejections.clear();
  16. }, 60000);

串联Promise

每次调用then()方法或catch()方法时实际上创建并返回了另一个Promise,只有当第一个Promise完成或被拒绝后,第二个才会被解决。请看以下这个示例:

  1. let p1 = new Promise(function (resolve, reject) {
  2. resolve(42);
  3. });
  4. p1.then(function (value) {
  5. console.log(value); //42
  6. }).then(function () {
  7. console.log("Finished"); //err
  8. })

调用p1.then()后返回第二个Promise,紧接着又调用了它的then()方法,只有当第一个Promise被解决之后才会调用第二个then()方法的完成处理程序。如果将这个示例拆解开,看起来是这样的:

  1. let p1 = new Promise(function (resolve, reject) {
  2. resolve(42);
  3. });
  4. let p2 = p1.then(function (value) {
  5. console.log(value);
  6. })
  7. p2.then(function () {
  8. console.log("Finished");
  9. });

在这个非串联版本的代码中,调用p1.then()的结果被存储在了p2中,然后p2.then()被调用来添加最终的完成处理程序。你可能已经猜到,调用p2.then()返回的也是一个Promise,只是在此示例中我们并未使用它。

捕获错误

在之前的示例中,完成处理程序或拒绝处理程序中可能发生错误,而Promise链可以用来捕获这些错误。例如:

  1. let p1 = new Promise(function (resolve, reject) {
  2. resolve(42);
  3. });
  4. p1.then(function (value) {
  5. throw new Error("Boom!");
  6. }).catch(function (err) {
  7. console.log(error.message);
  8. })

在这段代码中,p1的完成处理程序抛出了一个错误,链式调用第二个Promise的catch()方法后,可以通过它的拒绝处理程序接收这个错误。如果拒绝处理程序抛出错误,也可以通过相同的方式接收到这个错误:

  1. let p1 = new Promise(function (resolve, reject) {
  2. throw new Error("err");
  3. });
  4. p1.catch(function (err) {
  5. console.log(err.message); //"err"
  6. throw new Error("Boom!");
  7. }).catch(function (err) {
  8. console.log(error.message); //"Boom"
  9. });

此处的执行器抛出错误并触发Promise p1的拒绝处理程序,这个处理程序又抛出另外一个错误,并且被第二个Promise的拒绝处理程序捕获。链式Promise调用可以感知到链中其他Promise的错误。

注:务必在Promise链的末尾留有一个拒绝处理程序以确保能够正确处理所有可能发生的错误。
**

Promise链的返回值

Promise链的另一个重要特性是可以给下游Promise传递数据,我们已经看到了从执行器resolve()处理程序到Promise完成处理程序的数据传递过程,如果在完成处理程序中指定一个返回值,则可以沿着这条链继续传递数据。例如:

  1. let p1 = new Promise(function (resolve, reject) {
  2. resolve(42);
  3. });
  4. p1.then(function (value) {
  5. console.log(value); //42
  6. return value + 1;
  7. }).then(function (value) {
  8. console.log(value); //43
  9. });

等价于

  1. let p1 = new Promise(function (resolve, reject) {
  2. reject(42);
  3. });
  4. p1.catch(function (value) {
  5. //第一个完成处理程序
  6. console.log(value);
  7. return value + 1;
  8. }).then(function (value) {
  9. //第二个完成处理程序
  10. console.log(value);
  11. })

在Promise链中返回Promise

  1. let p1 = new Promise(function (resolve, reject) {
  2. resolve(42);
  3. });
  4. let p2 = new Promise(function (resolve, reject) {
  5. resolve(43);
  6. });
  7. p1.then(function (value) {
  8. //第一个完成处理程序
  9. console.log(value); //42
  10. return p2;
  11. }).then(function (value) {
  12. //第二个完成处理程序
  13. console.log(value); //43
  14. })

等价于

  1. let p1 = new Promise(function (resolve, reject) {
  2. resolve(42);
  3. });
  4. let p2 = new Promise(function (resolve, reject) {
  5. resolve(43);
  6. })
  7. let p3 = p1.then(function (value) {
  8. //第一个完成处理程序
  9. console.log(value);
  10. return 42;
  11. });
  12. p3.then(function (value) {
  13. //第二个完成处理程序
  14. console.log(value);
  15. });

如果你想在一个Promise被解决后触发另一个Promise,

  1. let p1 = new Promise(function (resolve, reject) {
  2. resolve(42);
  3. });
  4. p1.then(function (value) {
  5. console.log(value); //42
  6. //创建一个新的promise
  7. let p2 = new Promise(function (resolve, reject) {
  8. resolve(43);
  9. });
  10. return p2
  11. }).then(function (value) {
  12. console.log(value); //43
  13. });

在p1的完成处理程序里创建了一个新的Promise,直到p2被完成才会执行第二个完成处理程序。

响应多个Promise

Promise.all()方法只接受一个参数并返回一个Promise,该参数是一个含有多个受监视Promise的可迭代对象(例如,一个数组),只有当可迭代对象中所有Promise都被解决后返回的Promise才会被解决,只有当可迭代对象中所有Promise都被完成后返回的Promise才会被完成,正如这个示例所示:

  1. let p1 = new Promise(function (resolve, reject) {
  2. resolve(42);
  3. });
  4. let p2 = new Promise(function (resolve, reject) {
  5. resolve(43);
  6. });
  7. let p3 = new Promise(function (resolve, reject) {
  8. resolve(44);
  9. });
  10. let p4 = Promise.all([p1, p2, p3]);
  11. p4.then(function (value) {
  12. console.log(Array.isArray(value)); //true
  13. console.log(value[0]);
  14. console.log(value[1]);
  15. console.log(value[2]);
  16. });

在这段代码中,每个Promise解决时都传入一个数字,调用Promise.all()方法创建Promise p4,最终当Promise p1、p2和p3都处于完成状态后p4才被完成。传入p4完成处理程序的结果是一个包含每个解决值(42、43和44)的数组,这些值按照Promise被解决的顺序存储,所以可以根据每个结果来匹配对应的Promise。

所有传入Promise.all()方法的Promise只要有一个被拒绝,那么返回的Promise没等所有Promise都完成就立即被拒绝:

  1. let p1 = new Promise(function (resolve, reject) {
  2. resolve(42);
  3. });
  4. let p2 = new Promise(function (resolve, reject) {
  5. reject(43);
  6. });
  7. let p3 = new Promise(function (resolve, reject) {
  8. resolve(44);
  9. });
  10. let p4 = Promise.all([p1, p2, p3]);
  11. p4.then(function (value) {
  12. console.log(Array.isArray(value)); //false
  13. console.log(value);//43
  14. });

image.png

Promise.race()方法

Promise.race()方法监听多个Promise的方法稍有不同:它也接受含多个受监视Promise的可迭代对象作为唯一参数并返回一个Promise,但只要有一个Promise被解决返回的Promise就被解决,无须等到所有Promise都被完成。一旦数组中的某个Promise被完成,Promise.race()方法也会像Promise.all()方法一样返回一个特定的Promise,例如:

  1. let p1 = Promise.resolve(42);
  2. let p2 = new Promise(function (resolve, reject) {
  3. resolve(43);
  4. });
  5. let p3 = new Promise(function (resolve, reject) {
  6. resolve(44);
  7. });
  8. let p4 = Promise.race([p1, p2, p3]);
  9. p4.then(function (value) {
  10. console.log(value);//42
  11. });

在这段代码中,p1创建时便处于已完成状态,其他Promise用于编排任务。随后,p4的完成处理程序被调用并传入值42,其他Promise则被忽略。实际上,传给Promise.race()方法的Promise会进行竞选,以决出哪一个先被解决,如果先解决的是已完成Promise,则返回已完成Promise;如果先解决的是已拒绝Promise,则返回已拒绝Promise。这里是一段拒绝示例:

  1. let p1 = new Promise(function (resolve, reject) {
  2. setTimeout(function () {
  3. resolve(42);
  4. }, 0);
  5. });
  6. let p2 = Promise.reject(43);
  7. let p3 = new Promise(function (resolve, reject) {
  8. resolve(44);
  9. });
  10. let p4 = Promise.race([p1, p2, p3]);
  11. p4.catch(function (value) {
  12. console.log(value);//43
  13. });

此时,由于p2已处于被拒绝状态,因而当Promise.race()方法被调用时p4也被拒绝了,尽管p1和p3最终被完成,但由于是发生在p2被拒后,因此它们的结果被忽略掉。

自Promise继承

Promise与其他内建类型一样,也可以作为基类派生其他类,所以你可以定义自己的Promise变量来扩展内建Promise的功能。例如,假设你想创建一个既支持then()方法和catch()方法又支持success()方法和failure()方法的Promise,则可以这样创建该Promise类型

  1. class MyPromise extends Promise {
  2. //使用默认的构造函数
  3. success(resolve, reject) {
  4. return this.then(resolve, reject);
  5. }
  6. failure(reject) {
  7. return this.catch(reject);
  8. }
  9. }
  10. let promise = new MyPromise(function (resolve, reject) {
  11. resolve(42);
  12. });
  13. promise.success(function (value) {
  14. console.log(value); //42
  15. }).failure(function (value) {
  16. console.log(value);
  17. });

在这个示例中,派生自Promise的MyPromise扩展了另外两个方法:模仿resolve()的success()方法以及模仿reject()的failure()方法。
这两个新增方法都通过this来调用它模仿的方法,派生Promise与内建Promise的功能一样,只不过多了success()和failure()这两个可以调用的方法。
由于静态方法会被继承,因此派生的Promise也拥有MyPromise.resolve()、MyPromise.reject()、MyPromise.race()和MyPromise.all()这4个方法,后二者与内建方法完全一致,而前二者却稍有不同。
由于MyPromise.resolve()方法和MyPromise.reject()方法通过Symbol.species属性(参见第9章)来决定返回Promise的类型,故调用这两个方法时无论传入什么值都会返回一个MyPromise的实例。如果将内建Promise作为参数传入其他方法,则这个Promise将被解决或拒绝,然后该方法将会返回一个新的MyPromise,于是就可以给它的成功处理程序及失败处理程序赋值。例如:

  1. let p1 = new Promise(function (resolve, reject) {
  2. resolve(42);
  3. });
  4. let p2 = MyPromise.resolve(p1);
  5. p2.success(function (value) {
  6. console.log(value); //42
  7. });
  8. console.log(p2 instanceof MyPromise) //true

基于Promise的异步任务执行

  1. let fs = require("fs");
  2. function run(taskDef) {
  3. // 创建迭代器
  4. let task = taskDef();
  5. //开始执行任务
  6. let result = task.next();
  7. //递归函数遍历
  8. (function step() {
  9. // 如果有更多任务要做
  10. if (!result.done) {
  11. //用一个Promise来解决简化问题
  12. let promise = Promise.resolve(result.value);
  13. promise.then(function (value) {
  14. result = task.next(value);
  15. step();
  16. }).catch(function (error) {
  17. result = task.throw(error());
  18. step();
  19. });
  20. }
  21. }());
  22. }
  23. //定义一个可用于任务执行器的函数
  24. function readFile(filename) {
  25. return new Promise(function (resolve, reject) {
  26. fs.readFile(filename, function (err, content) {
  27. if (err) {
  28. reject(err);
  29. } else {
  30. resolve(contents);
  31. }
  32. });
  33. });
  34. }
  35. //执行一个任务
  36. run(function* () {
  37. let contents = yield readFile("config.json");
  38. doSomethingWith(contents);
  39. console.log("Done");
  40. });

未来的异步任务执行

JavaScript正在引入一种用于执行异步任务的更简单的语法,例如,await语法致力于替代之前章节中基于Promise的示例。其基本思想是用async标记的函数代替生成器,用await代替yield来调用函数,例如:

  1. (async function () {
  2. let contents = await readFile("config.json");
  3. doSomethingWith(contents);
  4. console.log("Done");
  5. });

小结

Promise的设计目标是改进JavaScript中的异步编程,它能够让你更好地掌控并组合多个同步操作,比事件系统和回调更实用。Promise编排的任务会被添加到JavaScript引擎任务队列并在未来执行,还有一个任务队列用于跟踪Promise的完成处理程序和拒绝处理程序并确保正确执行。
Promise有3个状态:进行中(pending)、已完成(fulfilled)和已拒绝(rejected)。Promise的起始状态是进行中,执行成功会变为已完成,失败则变为已拒绝。在后两种情况下你都可以添加处理程序,以便当Promise被解决(settled)时做出相应操作,通过then()方法可以添加完成处理程序或拒绝处理程序,通过catch()方法只能添加拒绝处理程序。
有多种方法可将Promise链接在一起并在它们之间传递信息,每次调用then()方法会创建并返回一个新的Promise,它会随前面Promise被解决而解决。这样的链条可用于触发一系列同步事件的响应,也可通过Promise.race()方法和Promise.all()方法来监控多个Promise的进程并做出相应响应。
由于Promise可以提供异步操作能返回的通用接口,因而将生成器和Promise结合后会进一步简化异步任务执行的过程,随后可用生成器和yield运算符来等待异步响应并做出相应反应。
大多数新的Web API都基于Promise构建,可以预期在未来会有更多的效仿者出现。