ES6 新增的 Promise (期约)引用类型, 支持优雅定义异步逻辑 ;
ES8 新增的 async & await 同样可以定义异步函数的机制

同步行为和异步行为的对立统一是计算机科学的一个基本概念。特别是在JavaScript 这种单线程事件循环模型中,同步操作与异步操作更是代码所要依赖的核心机制。

异步编程
异步行为是为了优化因计算量大而时间长的操作。如果在等待其他操作完成的同时,即使运行其他指令,系统也能保持稳定,那么这样做就是务实的。

重要的是,异步操作并不一定计算量大或要等很长时间。只要你不想为等待某个异步操作而阻塞线程执行,那么任何时候都可以使用。

同步与异步

同步的例子:
同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。

  1. let x = 3;
  2. x = x + 4;

异步例子:
异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作经常是必要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。如果代码要访问一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待。

  1. let x = 3
  2. setTimeout(() => x = x + 4, 1000)

以往的异步编程模式

在早期的JavaScript 中,只支持定义回调函数来表明异步操作完成。串联多个异步操作是一个常见的问题,通常需要深度嵌套的回调函数(俗称“回调地狱”)来解决。

  1. function double(value) {
  2. setTimeout(() => setTimeout(console.log, 0, value*2), 1000)
  3. }
  4. double(3);
  5. // 6(大约1000 毫秒之后)

异步返回值

  1. function double(value, callback) {
  2. setTimeout(() => callback(value * 2), 1000)
  3. }
  4. double(3, (x) => console.log(`I was given : ${x}`))

JS 运行时在1s 后把一个函数推送到 消息队列上。 这个函数会又运行时 负责异步调度执行

失败处理

异步操作处理失败 & 成功 都应该具有回调

  1. function double(value, success, failure) {
  2. setTimeout(() => {
  3. try {
  4. // 成功的会调用
  5. if (typeof value !== 'number') {
  6. // throw 抛出错误
  7. throw 'Must provide number as first argument';
  8. }
  9. success(2 * value);
  10. } catch (e) {
  11. // 失败的回调
  12. // e 就是 throw 定义的错误数据
  13. failure(e);
  14. }
  15. }, 1000);
  16. }
  17. const successCallback = (x) => console.log(`Success: ${x}`);
  18. const failureCallback = (e) => console.log(`Failure: ${e}`);
  19. double(3, successCallback, failureCallback);
  20. double('b', successCallback, failureCallback);
  21. // Success: 6(大约1000 毫秒之后)
  22. // Failure: Must provide number as first argument(大约1000 毫秒之后)

嵌套异步回调

如果异步返值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂。

  1. function double(value, success, failure) {
  2. setTimeout(() => {
  3. try {
  4. if (typeof value !== 'number') {
  5. throw 'Must provide number as first argument';
  6. }
  7. success(2 * value);
  8. } catch (e) {
  9. failure(e);
  10. }
  11. }, 1000);
  12. }
  13. const successCallback = (x) => {
  14. double(x, (y) => console.log(`Success: ${y}`));
  15. };
  16. const failureCallback = (e) => console.log(`Failure: ${e}`);
  17. // 调用
  18. double(3, successCallback, failureCallback);
  19. // Success: 12(大约1000 毫秒之后)

Promise 期约

ES6 新增的引用类型 Promise , 可以通过 new 操作符来实例化。

  1. let p = new Promise(() => {}) // 参数:执行器(executor)函数
  2. setTimeout(console.log, 0, p)

Promise状态

  • 待定 (pending
  • 兑现 (fulfilled, 也曾为解决, resolved
  • 拒绝 (rejected

待定(pending)是期约的最初始状态 ,
在待定状态下,期约可以落定(settled)为代表成功的兑现(fulfilled)状态,或者代表失败的拒绝(rejected)状态

无论是落定为那种状态, 都是不可逆的。 只要从待定转换为兑现或拒绝,期约的状态就不再改变。而且,
也不能保证期约必然会脱离待定状态。
组织合理的代码无论期约解决(resolve)还是拒绝(reject),甚至永远处于待定(pending)状态

期约的状态是私有的,不能直接通过JavaScript 检测到
另外,期约的状态也不能被外部JavaScript 代码修改

这与不能读取该状态的原因是一样的:期约故意将异步行为封装起来,从而隔离外部的同步代码。

Primise 用例

期约主要有两大用途。首先是抽象地表示一个异步操作。期约的状态代表期约是否完成。“待定”表示尚未开始或者正在执行中。“兑现”表示已经成功完成,而“拒绝”则表示没有成功完成。

  • 发起HTTP请求,根据返回的状态码,Promise状态根据状态码改变
  • 请求获取数据, 根据HTTP状态码,返回的 JSON & Error 对象包含HTTP状态码和相关错误信息

根据这两种用例,只要Promise状态为 fulfilled,就会有一个私有的内部值 valuerejected 时, 就会有一个私有的内部理由 reason

无论是值还是理由,都是包含原始值或对象的不可修改的引用
二者都是可选的,而且默认值为undefined。在期约到达某个落定状 态时执行的异步代码始终会收到这个值或理由。

Promise的使用

Promiese 执行器(一个函数)用处 (Promiese 的一二个参数)

  • 初始化Promiese 的异步行为
  • 控制状态的最终转换

控制期约状态的转换是通过调用它的两个函数参数实现的, resolve() & reject()

  • 调用 resolve()会把状态切换为 fulfulled 兑现
  • 调用 reject() 会把状态切换为拒绝 rejected, 另外,调用reject()也会抛出错误(后面会讨论这个错误)。
  1. let p1 = new Promise((resolve, reject) => resolve())
  2. setTimeout(console.log, 0, p1) // // Promise <resolved>
  3. let p2 = new Promise((resolve, reject) => reject());
  4. setTimeout(console.log, 0, p2); // Promise <rejected>
  5. // Uncaught error (in promise)

以上的代码是,Promise执行器是同步执行的。

  1. // 这里代码安装顺序执行
  2. new Promise(() => setTimeout(console.log, 0, 'executor'));
  3. setTimeout(console.log, 0, 'promise initialized');
  4. // executor
  5. // promise initialized
  1. // 添加 setTimeout 可以推迟切换状态
  2. let p = new Promise((resole, reject) => setTimeout(resole, 1000))
  3. // 在console.log 打印期约实例的时候,还不会执行超时回调(即resolve())
  4. setTimeout(console.log, 0, p); // Promise <pending>

无论resolve()和reject()中的哪个被调用,状态转换都不可撤销了。于是继续修改状态会静默失败,如下所示

  1. let p = new Promise((resolve, reject) => {
  2. resolve()
  3. reject() // 没有效果
  4. })
  5. setTimeout(console.log, 0, p); // Promise <resolved>

为避免期约卡在待定状态,可以添加一个定时退出功能。比如,可以通过setTimeout 设置一个10 秒钟后无论如何都会拒绝期约的回调:
超时退出

  1. let p = new Promise((resolve, reject) => {
  2. setTimeout(reject, 1000 * 10) // 10s 后调用
  3. })
  4. setTimeout(console.log, 0, p); // Promise <pending>
  5. setTimeout(console.log, 11000, p); // 11 秒后再检查状态
  6. // (After 10 seconds) Uncaught error
  7. // (After 11 seconds) Promise <rejected>

因为期约的状态只能改变一次,所以这里的超时拒绝逻辑中可以放心地设置让期约处于待定状态的最长时间。

Promise.resolve()

用过调用 Promise.resolve() 静态方法, 可以实例化一个解决(成功)的 Promise

  1. let p1 = new Promise((resolve, reject) => resolve())
  2. let p2 = Promise.resolve();
  3. // 这个解决的期约的值对应着传给Promise.resolve()的第一个参数
  4. setTimeout(console.log, 0, Promise.resolve());
  5. // Promise <resolved>: undefined
  6. // 传入成功的值,Promise.resolve(xxx)
  7. setTimeout(console.log, 0, Promise.resolve(3));
  8. // Promise <resolved>: 3
  9. // 多余的参数会忽略, 只接受第一个参数
  10. setTimeout(console.log, 0, Promise.resolve(4, 5, 6));
  11. // Promise <resolved>: 4

对这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此Promise.resolve()可以说是一个幂等方法

  1. let p = Promise.resolve(7)
  2. setTimeout(console.log, 0, p === Promise.resolve(p))
  3. // true
  4. // 类似一个空包装 -> 无论包装多少层
  5. setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p)));
  6. // true

这个幂等性会保留传入期约的状态:

  1. let p = new Promise(() => {}) // pending
  2. setTimeout(console.log, 0, p) // Promise <pending>
  3. setTimeout(console.log, 0, Promise.resolve(p)); // Promise <pending>
  4. setTimeout(console.log, 0, p === Promise.resolve(p)); // true

Promise.resolve()静态方法能够包装任何非期约值,包括错误对象,并将其转换为解决的期约

  1. let p = new Promise.resolve(new Error("foo")) // 传入 错误对象
  2. setTimeout(console.log, 0, p) // Promise <fulfilled>: Error: foo
  3. // 一个成功的错误对象

Promise.reject()

Promise.resolve()类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误(这个错误不能通过try/catch 捕获,而只能通过拒绝处理程序捕获)

  1. let p1 = new Promise((resolve, reject) => reject())
  2. let p2 = Promise.reject()
  3. //这个拒绝的期约的理由就是传给Promise.reject()的第一个参数。这个参数也会传给后续的拒绝处理程序:
  1. let p = Promise.reject(3)
  2. setTimeout(console.log, 0, p) // Promise {<rejected>: 3}
  3. // 通过 then, 捕获 错误信息
  4. p.then(null, (e) => setTimeout(console.log, 0, e)); // 3

关键在于,Promise.reject()并没有照搬Promise.resolve()的幂等逻辑。如果给它传一个期约对象,则这个期约会成为它返回的拒绝期约的理由:

  1. setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
  2. // Promise <rejected>: Promise <resolved>

同步 & 异步 执行的二次元

Promise.reject() 不能被try/catch捕获到

  1. // 同步
  2. try {
  3. throw new Error('foo');
  4. } catch(e) {
  5. console.log(e); // Error: foo
  6. }
  7. // 异步
  8. try {
  9. Promise.reject(new Error('bar'));
  10. } catch(e) {
  11. console.log(e);
  12. }
  13. // Uncaught (in promise) Error: bar

第一个try/catch 抛出并捕获了错误,第二个try/catch 抛出错误却没有捕获到。

Promise 的实例方法

期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁 , 这些方法可以访问异步操作返回的数据,处理期约成功和失败的结果,连续对期约求值,或者添加只有期约进入终止状态时才会执行的代码。

实现 Thenable 接口

在ECMAScript 暴露的异步结构中,任何对象都有一个then()方法。这个方法被认为实现了Thenable 接口。

  1. class MyThenable {
  2. then() {}
  3. }

Promise 类型实现了 Thenable接口。这个简化的接口跟TypeScript 或其他包中的接口或类型定义不同,它们都设定了Thenable 接口更具体的形式。

Promise.prototype.then()

Promise.prototype.then()是为期约实例添加处理程序的主要方法。这个then()方法接收最多两个参数:onResolved处理程序和onRejected处理程序。

  1. function onResolved(id) {
  2. setTimeout(console.log, 0, id, 'resolved')
  3. }
  4. function onRejected(id) {
  5. setTimeout(console.log, 0, id, 'rejected')
  6. }
  7. let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000))
  8. let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));
  9. p1.then(
  10. () => onResolved('p1'),
  11. () => onRejected('p1')
  12. );
  13. p2.then(
  14. () => onResolved('p2'),
  15. () => onRejected('p2')
  16. );
  17. //(3 秒后)
  18. // p1 resolved
  19. // p2 rejected

因为期约只能转换为最终状态一次,所以这两个操作一定是互斥的。

  1. function onResolved(id) {
  2. setTimeout(console.log, 0, id, "resolved")
  3. }
  4. function onRejected(id) {
  5. setTimeout(console.log, 0, id, "rejected")
  6. }
  7. let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000))
  8. let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000))
  9. // 非函数处理程序会被静默忽略,不推荐
  10. p1.then('gobbeltygook');
  11. // 不传onResolved 处理程序的规范写法
  12. p2.then(null, () => onRejected('p2'));
  13. // p2 rejected(3 秒后)

Promise.prototype.then() 方法返回一个新的期约实例

  1. let p1 = new Promise(() => {}); // undefined , 因为没有返回的函数语句
  2. let p2 = p1.then();
  3. setTimeout(console.log, 0, p1); // Promise <pending> // 状态就是 pending
  4. setTimeout(console.log, 0, p2); // Promise <pending>
  5. setTimeout(console.log, 0, p1 === p2); // false

换句话说,该处理程序的返回值会通过Promise.resolve()包装来生成新期约。如果没有提供这个处理程序,则Promise.resolve()就会包装上一个期约解决之后的值
如果没有显式的返回语句,则Promise.resolve()会包装默认的返回值undefined

  1. // 当 Promise.resolve() 具有值时
  2. let p1 = Promise.resolve('foo')
  3. // 若调用then()时不传处理程序,则原样向后传
  4. let p2 = p1.then() // 得到一个Promise 的成功的转台,并且具有返回的 foo 值
  5. setTimeout(console.log, 0, p2); // Promise <resolved>: foo
  6. // 这些都一样
  7. let p3 = p1.then(() => undefined);
  8. let p4 = p1.then(() => {});
  9. let p5 = p1.then(() => Promise.resolve());
  10. setTimeout(console.log, 0, p3); // Promise <resolved>: undefined
  11. setTimeout(console.log, 0, p4); // Promise <resolved>: undefined
  12. setTimeout(console.log, 0, p5); // Promise <resolved>: undefined
  13. // 这些都一样
  14. let p6 = p1.then(() => 'bar');
  15. let p7 = p1.then(() => Promise.resolve('bar'));
  16. setTimeout(console.log, 0, p6); // Promise <resolved>: bar
  17. setTimeout(console.log, 0, p7); // Promise <resolved>: bar
  18. // Promise.resolve()保留返回的期约
  19. let p8 = p1.then(() => new Promise(() => {}));
  20. let p9 = p1.then(() => Promise.reject());
  21. // Uncaught (in promise): undefined
  22. setTimeout(console.log, 0, p8); // Promise <pending> 没有状态
  23. setTimeout(console.log, 0, p9); // Promise <rejected>: undefined // reject失败,值为undefined
  24. // 如果使用 throw
  25. // 抛出异常会返回拒绝的期约
  26. let p10 = p1.then(() => { throw 'baz'; });
  27. // Uncaught (in promise) baz
  28. setTimeout(console.log, 0, p10); // Promise <rejected> baz
  29. // 注意,返回错误值不会触发上面的拒绝行为,而会把错误对象包装在一个解决的期约中:
  30. let p11 = p1.then(() => Error('qux'));
  31. setTimeout(console.log, 0, p11) // Promise <rejected> : Error: qux

onRejected 处理程序也与之类似:onRejected 处理程序返回的值也会被Promise.resolve()包装。拒绝处理程序在捕获错误后不抛出异常是符合期约的行为,应该返回一个解决期约。

  1. let p1 = Promise.reject("foo")
  2. // 调用then()时不传处理程序则原样向后传
  3. let p2 = p1.then();
  4. setTimeout(console.log, 0, p2); // Promise <rejected>: foo 什么都不传,就得到原本的reject 的eroor
  5. // 这些都一样
  6. let p3 = p1.then(null, () => undefined);
  7. let p4 = p1.then(null, () => {});
  8. let p5 = p1.then(null, () => Promise.resolve());
  9. // 得到成功的 Promise
  10. setTimeout(console.log, 0, p3); // Promise <resolved>: undefined
  11. setTimeout(console.log, 0, p4); // Promise <resolved>: undefined
  12. setTimeout(console.log, 0, p5); // Promise <resolved>: undefined
  13. // 这些都一样
  14. let p6 = p1.then(null, () => 'bar');
  15. let p7 = p1.then(null, () => Promise.resolve('bar'));
  16. setTimeout(console.log, 0, p6); // Promise <resolved>: bar
  17. setTimeout(console.log, 0, p7); // Promise <resolved>: bar
  18. // Promise.resolve()保留返回的期约
  19. let p8 = p1.then(null, () => new Promise(() => {}));
  20. let p9 = p1.then(null, () => Promise.reject());
  21. // Uncaught (in promise): undefined
  22. setTimeout(console.log, 0, p8); // Promise <pending>
  23. setTimeout(console.log, 0, p9); // Promise <rejected>: undefined rejected 状态
  24. let p10 = p1.then(null, () => { throw 'baz'; });
  25. // Uncaught (in promise) baz
  26. setTimeout(console.log, 0, p10); // Promise <rejected>: baz
  27. let p11 = p1.then(null, () => Error('qux'));
  28. setTimeout(console.log, 0, p11); // Promise <resolved>: Error: qux 成功的 Error

Promise.prototype.catch()

Promise.prototype.catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数:onRejected处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用Promise.prototype.then(null, onRejected)

  1. let p = new Promise.resolve();
  2. let onRejected = function(e) {
  3. setTimeout(console.log, 0, 'rejected');
  4. };
  5. // 这两种添加拒绝处理程序的方式是一样的:
  6. p.then(null, onRejected) // rejected
  7. p.catch(onRejected) // rejected

Promise.prototype.catch()返回一个新的期约实例:

  1. let p1 = new Promise(() => {});
  2. let p2 = p1.catch(); // 和 then() 方法一样
  3. setTimeout(console.log, 0, p1); // Promise <pending>
  4. setTimeout(console.log, 0, p2); // Promose <pending>
  5. // 二者不相等
  6. setTimeout(console.log, 0, p1 === p2); // false

在返回新期约实例方面,Promise.prototype.catch()的行为与Promise.prototype.then()的onRejected 处理程序是一样的。

Promise.prototype.finally()

finally() 在处理程序的Promise状态时,无论时解决&拒绝 都会执行 。
这个方法可以避免onResolved 和onRejected 处理程序中出现冗余代码。

  1. let p1 = Promise.resolve();
  2. let p2 = Promise.reject()
  3. let onFinally = function () {
  4. setTimeout(console.log, 0, "Finally!!!")
  5. }
  6. p1.finally(onFinally); // Finally
  7. p2.finally(onFinally); // finally

Promise.prototype.finally()方法返回一个新的期约实例:

  1. let p1 = new Promise(() => {});
  2. let p2 = p1.finally()
  3. setTimeout(console.log, 0, p1); // Promise <pending>
  4. setTimeout(console.log, 0, p2); // Promise <pending> // 返回值是 Promise
  5. setTimeout(console.log, 0, p1 === p2); // false

finally() 的使用场景 : 用在异步编程父级Promise与子级Promise之间的数据传递

  1. // 父级 Promise
  2. let p1 = Promise.resolve('foo');
  3. // 这里都会原样后传
  4. let p2 = p1.finally();
  5. setTimeout(console.log, 0, p2); // Promise <resolved>: foo
  6. let p3 = p1.finally(() => undefined) // 这里开始不同
  7. setTimeout(console.log, 0, p3); // Promise <resolved>: foo
  8. let p4 = p1.finally(() => {});
  9. setTimeout(console.log, 0, p4); // Promise <resolved>: foo
  10. let p5 = p1.finally(() => Promise.resolve());
  11. setTimeout(console.log, 0, p5); // Promise <resolved>: foo
  12. let p6 = p1.finally(() => 'bar');
  13. setTimeout(console.log, 0, p6); // Promise <resolved>: foo
  14. let p7 = p1.finally(() => Promise.resolve('bar'));
  15. setTimeout(console.log, 0, p7); // Promise <resolved>: foo
  16. let p8 = p1.finally(() => Error('qux'));
  17. setTimeout(console.log, 0, p8); // Promise <resolved>: foo

如果返回的是一个待定的期约,或者onFinally处理程序抛出了错误(显式抛出或返回了一个拒绝期约),则会返回相应的期约(待定或拒绝)

  1. // Promise.resolve()保留返回的期约
  2. let p9 = p1.finally(() => new Promise(() => {}));
  3. let p10 = p1.finally(() => Promise.reject());
  4. // Uncaught (in promise): undefined
  5. setTimeout(console.log, 0, p9); // Promise <pending>
  6. setTimeout(console.log, 0, p10); // Promise <rejected>: undefined
  7. let p11 = p1.finally(() => { throw 'baz'; });
  8. // Uncaught (in promise) baz
  9. setTimeout(console.log, 0, p11); // Promise <rejected>: baz

返回待定期约的情形并不常见,这是因为只要期约一解决,新期约仍然会原样后传初始的期约:

  1. let p1 = Promise.resolve('foo');
  2. // 忽略解决的值
  3. let p2 = p1.finally(
  4. () => new Promise((resolve, reject) => setTimeout(() => resolve('bar'), 100)));
  5. setTimeout(console.log, 0, p2); // Promise <pending>
  6. setTimeout(() => setTimeout(console.log, 0, p2), 200);
  7. // 200 毫秒后:
  8. // Promise <resolved>: foo