Promise

ES6 新增的引用类型 Promise,可以通过 new 操作符来实例化,创建时需要传入执行器(executor)函数作为参数,如果不传会报错。

let p = new Promise()
// Uncaught TypeError: Promise resolver undefined is not a function

// 传入空函数
let p = new Promise(() => {});
console.log(p);
// Promise {<pending>}

Promise 是一个有三种状态的对象:

  • 待定 pending
  • 兑现 fulfilled(也可成为“解决”,resolved)
  • 拒绝 rejected

注意:fulfilled 中 ful 是一个 L,兑现和拒绝都是被动式。

基础

通过执行函数控制 Promise 状态

由于状态是私有的,所以只能通过内部操作,即内部操作在执行函数中完成。执行器函数有两项职责:初始化 Promise 的异步行为和控制状态的最终转换。其中,控制状态的转换是通过调用它的两个函数参数实现的,分别为 resolve 和 reject。

执行器函数是同步执行的,因为执行器函数是 Promise 的初始化程序。

new Promise(() => {
  console.log('执行器内部');
  setTimeout(console.log, 0, 'executor');
});
console.log('主程序');
setTimeout(console.log, 0, 'promise initialized');
// 执行器内部
// 主程序
// executor
// promise initialized

无论 resolve 和 reject 中的哪个被调用,状态转换不可撤销,继续修改会静默失败。

let p = new Promise((res, rej) => {
  res();
  rej(); // 没有效果
});
setTimeout(console.log, 0, p); // Promise <fulfilled>: undefined

Promise.resolve

Promise 并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。

通过调用 Promise.resolve() 静态方法,可以实例化一个解决的 Promise。下面两个 Promise 实例实际上是一样的:

let p1 = new Promise((res, rej) => res());
let p2 = Promise.resolve();

可以把解决的 Promise 的值传给 Promise.resolve() 的第一个参数。使用这个静态方法,实际上可以把任何职都转换为一个 Promise:

console.log(Promise.resolve());
// Promise <fulfilled>: undefined

console.log(Promise.resolve(3));
// Promise <fulfilled>: 3

console.log(Promise.resolve(Promise.resolve(3)));
// Promise <fulfilled>: 3

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

let p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p));
// true

setTimeout(console.log, 0, Promise.resolve(Promise.resolve(p)));
// true

Promise.reject

与 Promise.resolve 类似,会实例化一个拒绝的 Promise 并抛出一个异步错误(这个错误不能被 try/catch 捕获,而只能通过拒绝处理程序捕获)。下面两个实例其实是一样的:

let p1 = new Promise((res, rej) => rej());
let p2 = Promise.reject();

这个拒绝的 Promise 的理由就是传给 Promise.reject 的第一个参数,这个参数也会传给后续的拒绝处理程序:

let p = Promise.reject(3);
console.log(p); // Promise <rejected>: 3

p.then(null, (e) => setTimeout(console.log, 0, e)); // 3

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

setTimeout(console.log, 0, Promise.reject(Promise.resolve(3)));
// Promise {<rejected>: Promise}

同步/异步执行的二元性

Promise 的设计很大程度上会导致一种完全不同于 JavaScript 的计算模式,其中包含了两种模式下抛出错误的情形:

try {
  throw new Error('foo');
} catch(e) {
  console.log(e); // Error: foo
}

try {
  Promise.reject(new Error('bar'));
} catch(e) {
  console.log(e);
}
// Promise {<rejected>: Error: bar}
// Uncaught (in promise) Error: bar

第一个 try/catch 抛出并捕获了错误,第二个却没有捕获到。乍一看这可能有点违反直觉,因为代码中确实创建了一个拒绝的 Promise 实例,而这个实例也抛出了包含拒绝理由的错误。这里的同步代码之所以没有捕获 Promise 抛出的错误,是因为他没有通过异步模式捕获错误。从这里就可以看出 Promise 真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介。

这个例子,拒绝 Promise 的错误并没有跑到执行同步代码的线程里,而是通过浏览器异步消息队列来处理。因此 try/catch 块不能捕获错误。代码一旦开始以异步模式执行,则唯一与之交互的方式就是使用异步结构——更具体地说,就是 Promise 的方法。

Promise.prototype.then

这个方法接收最多两个参数:onResolved 和 onRejected 处理程序,都是可选的。而且,传给 then 的任何非函数类型的参数都会被静默忽略。如果只提供 onRejected 参数,那就要在 onResolved 参数位置上传 undefined 有助于避免在内存中创建多余的对象。

Promise.prototype.then 方法返回一个新的 Promise 实例:

let p1 = new Promise(() => {});
let p2 = p1.then(); // 如果调用 then 时不传处理程序,则原样往后传
setTimeout(console.log, 0, p1);
// Promise <pending>
setTimeout(console.log, 0, p2);
// Promise <pending>
setTimeout(console.log, 0, p1 === p2);
// false

如果 Promise 实例基于 onResolved 处理程序的返回值构建,该处理程序的返回值会通过 Promise.resolve() 包装来生成新 Promise。如果没有提供这个处理程序,则 Promise.resolve() 就会包装上一个 Promise 解决之后的值。如果没有显示的返回语句,则 Promise.resolve() 会包装默认的返回值 undefined。

let p1 = Promise.resolve('foo');
let p2 = p1.then();
setTimeout(console.log, 0, p2); // Promise <fulfilled>: 'foo'

// 这些都一样
let p3 = p1.then(() => undefined);
let p4 = p1.then(() => {});
let p5 = p1.then(() => Promise.resolve());

setTimeout(console.log, 0, p3); // Promise <fulfilled>: undefined
setTimeout(console.log, 0, p4); // Promise <fulfilled>: undefined
setTimeout(console.log, 0, p5); // Promise <fulfilled>: undefined

如果有显示的返回值,则 Promise.resolve() 会包装这个值:

let p1 = Promise.resolve('foo');
// 这些都一样
let p6 = p1.then(() => 'bar');
let p7 = p1.then(() => Promise.resolve('bar'));

setTimeout(console.log, 0, p6); // Promise <fulfilled>: 'bar'
setTimeout(console.log, 0, p7); // Promise <fulfilled>: 'bar'

// Promise.resolve() 保留返回的 Promise
let p8 = p1.then(() => new Promise(() => {}));
let p9 = p1.then(() => Promise.reject());
// Uncaught (in promise): undefined

setTimeout(console.log, 0, p8); // Promise <pending>
setTimeout(console.log, 0, p9); // Promise <rejected>: undefined

抛出异常会返回拒绝的 Promise:(注意返回值)

let p1 = Promise.resolve('foo');
let p10 = p1.then(() => { throw 'baz' });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p10); // Promise <rejected>: baz

注意,返回错误值不会触发上面的拒绝行为,而会把错误对象包装在一个解决的 Promise 中:

let p1 = Promise.resolve('foo');
let p11 = p1.then(() => Error('qux'));
setTimeout(console.log, 0, p11); // Promise <fulfilled>: Error: qux

onRejected 处理程序也与之类似:onRejected 处理程序返回的值也会被 Promise.resolve() 包装。乍一看可能有点违反直觉,但想一想,onRejected 处理程序的任务不就是捕获异步错误吗?因此,拒绝处理程序在捕获错误猴不抛出异常是符合 Promise 的行为,应该返回一个解决 Promise。

下面展示用 Promise.reject() 代替之前例子中的 Promise.resolve() 之后的结果:

let p1 = Promise.reject('foo');
let p2 = p1.then(); // 不传处理程序则原样后传
setTimeout(console.log, 0, p2); // Promise <rejected>: 'foo'

// 这些都一样
let p3 = p1.then(null, () => undefined);
let p4 = p1.then(null, () => {});
let p5 = p1.then(null, () => Promise.resolve());

setTimeout(console.log, 0, p3); // Promise <fulfilled>: 'foo'
setTimeout(console.log, 0, p4); // Promise <fulfilled>: 'foo'
setTimeout(console.log, 0, p5); // Promise <fulfilled>: 'foo'

// 这些都一样
let p6 = p1.then(null, () => 'bar');
let p7 = p1.then(null, () => Promise.resolve('bar'));

setTimeout(console.log, 0, p6); // Promise <fulfilled>: 'bar'
setTimeout(console.log, 0, p7); // Promise <fulfilled>: 'bar'

// Promise.resolve() 保留返回的 Promise
let p8 = p1.then(null, () => new Promise(() => {}));
let p9 = p1.then(null, () => Promise.reject());

setTimeout(console.log, 0, p8); // Promise <pending>
setTimeout(console.log, 0, p9); // Promise <rejected>: undefined

let p10 = p1.then(null, () => { throw 'baz' });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p10); // Promise <rejected>: 'baz'

let p11 = p1.then(null, () => Error('qux'));
setTimeout(console.log, 0, p11); // Promise <fulfilled>: Error: 'qux'

Promise.prototype.catch

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

下面代码展示同样的情况:

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

非重入 Promise 方法

当 Promise 进入落定状态时,与该状态相关的处理程序(onResolved/onRejected)仅仅会被排期,而非立即执行。跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行。即使 Promise 一开始就是与附加处理程序关联的状态,执行顺序也是这样。这个特性有 JS 运行时保证,被称为“非重入”特性——non-reentrancy。

let p = Promise.resolve(); // 创建解决状态的 Promise
// 添加解决处理程序
// 直觉上,这个处理程序会等 Promise 一解决就执行
p.then(() => console.log('onResovled handler'));
// 同步输出,证明 then() 已经返回
console.log('then() returns');

// 实际的输出:
// then() returns
// onResovled handler

在这个例子中,在一个解决 Promise 上调用 then() 会把 onResolved 处理程序推进消息队列。但这个处理程序在当前线程上的同步代码执行完成前不会执行。因此,跟在 then()后面的同步代码一定先于处理程序执行。

先添加处理程序(onResolved/onRejected)后解决 Promise 也是一样的。

let synchronousResolve;
// 创建一个 Promise 并将解决函数保存在一个局部变量中
let p = new Promise((res) => {
  synchronousResolve = function () {
    console.log('1: invoking resolve()');
    res();
    console.log('2: resolve returns');
  }
});

p.then(() => console.log('4: then() handler executes'));
synchronousResolve();
console.log('3: synchronousResolve() returns');

// 实际的输出:
// 1: invoking resolve() 
// 2: resolve() returns 
// 3: synchronousResolve() returns 
// 4: then() handler executes

这个例子中,即使 Promise 状态变化发生在添加处理程序之后,处理程序也会等到运行的消息队列让它出列时才会执行。“3: sync...” 打印在“4: then...”之前就说明问题了,为了清晰可以修改一下:

let synchronousResolve;
// 创建一个 Promise 并将解决函数保存在一个局部变量中
let p = new Promise((res) => {
  synchronousResolve = function () {
    console.log('1: invoking resolve()');
    res();
    console.log('2: resolve returns');
    return '3: sync returns';
  }
});

p.then(() => console.log('4: then() handler executes'));
console.log('同步1:', p); // <pending>
console.log(synchronousResolve());
console.log('同步2:', p); // <fulfilled>

// 实际输出:
// 同步1: Promise {<pending>}
// 1: invoking resolve()
// 2: resolve returns
// 3: sync returns
// 同步2: Promise {<fulfilled>: undefined}
// 4: then() handler executes

第15行都说明了 Promise 的状态改变了,但“4: then..”仍然是位于之后输出。

非重入 Promise 适用于 onResolved/onRejected 处理程序、catch 处理程序和 finally 处理程序。

let p1 = Promise.resolve(); 
p1.then(() => console.log('p1.then() onResolved')); 
console.log('p1.then() returns'); 
let p2 = Promise.reject(); 
p2.then(null, () => console.log('p2.then() onRejected')); 
console.log('p2.then() returns'); 
let p3 = Promise.reject(); 
p3.catch(() => console.log('p3.catch() onRejected')); 
console.log('p3.catch() returns'); 
let p4 = Promise.resolve(); 
p4.finally(() => console.log('p4.finally() onFinally')); 
console.log('p4.finally() returns');

// p1.then() returns 
// p2.then() returns 
// p3.catch() returns 
// p4.finally() returns 
// p1.then() onResolved 
// p2.then() onRejected 
// p3.catch() onRejected 
// p4.finally() onFinally

邻近处理程序的执行顺序

如果给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行。无论是 then()、catch() 还是 finally() 添加的处理程序都是如此。

let p1 = Promise.resolve();
let p2 = Promise.reject();

p1.then(() => setTimeout(console.log, 0, 1));
p1.then(() => setTimeout(console.log, 0, 2));

p2.then(null, () => setTimeout(console.log, 0, 3));
p2.then(null, () => setTimeout(console.log, 0, 4));

p2.catch(() => setTimeout(console.log, 0, 5));
p2.catch(() => setTimeout(console.log, 0, 6));

p1.finally(() => setTimeout(console.log, 0, 7));
p1.finally(() => setTimeout(console.log, 0, 8));
// 1 2 3 4 5 6 7 8 顺序多行输出(这里节省篇幅就写一行了)

传递解决值和拒绝理由

落定状态后,Promise 会提供其解决值(如果兑现)或其拒绝理由(如果拒绝)给相关状态的处理程序。拿到返回值猴,就可以进一步对这个值进行操作。比如两次网络请求,第一次返回的 JSON 是发送第二次请求必须的数据,那么该 JSON 应该传给 onResolved 处理程序继续处理。当然,失败的网络请求也应该把 HTTP 状态码传给 onRejected 处理程序。

在执行函数中,解决的值和拒绝的理由是分别作为 resolve() 和 reject() 的第一个参数往后传的。然后这些值又会传给他们各自的处理程序,作为 onResolved 或 onRejected 处理程序的唯一参数。

let p1 = new Promise((res, rej) => res('foo'));
p1.then((value) => console.log(value)); // foo

let p2 = new Promise((res, rej) => rej('bar'));
p2.catch((reason) => console.log(reason)); // bar

Promise.resolve 和 Promise.reject 在被调用时就会接收解决值和拒绝理由。同样地,它们返回的 Promise 也会像执行器一样把这些值传给 onResolved 或 onRejected 处理程序:

let p1 = Promise.resolve('foo');
p1.then((value) => console.log(value)); // foo

let p2 = Promise.reject('bar');
p1.catch((reason) => console.log(reason)); // bar

拒绝 Promise 与拒绝错误处理

拒绝 Promise 类似于 throw() 表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理。在 Promise 的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝理由。

因此以下这些 Promise 都会以一个错误对象为由被拒绝:

let p1 = new Promise((res, rej) => rej(Error('p1 foo')));
let p2 = new Promise((res, rej) => { throw Error('p2 foo') });
let p3 = Promise.resolve().then(() => { throw Error('p3 foo') });
let p4 = Promise.reject(Error('p4 foo'));

setTimeout(console.log, 0, p1);
setTimeout(console.log, 0, p2);
setTimeout(console.log, 0, p3);
setTimeout(console.log, 0, p4);
// 也会抛出 4 个未捕获的错误

Promise 可以以任何理由拒绝,包括 undefined,但最好统一使用错误对象。这样做主要是因为创建错误对象可以让浏览器捕获错误对象的栈追踪信息,而这些信息对调试是非常关键的。例如前面的例子抛出的 4 个错误栈追踪信息如下:

<!-- 把上面的代码写在一个空的 .html 文件中 -->
<script>
  // 省略以上的示例代码...(p1, p2, p3, p4)
  // 浏览器中会有如下输出:
  /*
  promise-错误捕获.html:2 Uncaught (in promise) Error: p1 foo
    at promise-错误捕获.html:2:42
    at new Promise (<anonymous>)
    at promise-错误捕获.html:2:12
  promise-错误捕获.html:4 Uncaught (in promise) Error: p2 foo
    at promise-错误捕获.html:4:11
    at new Promise (<anonymous>)
    at promise-错误捕获.html:3:12
  promise-错误捕获.html:9 Uncaught (in promise) Error: p4 foo
    at promise-错误捕获.html:9:27
  promise-错误捕获.html:7 Uncaught (in promise) Error: p3 foo
    at promise-错误捕获.html:7:11
    */
</script>

所有错误都是异步抛出且未处理的,通过错误对象捕获的栈追踪信息展示了错误发生的路径。注意错误的顺序:Promise.resolve().then() 的错误最后才出现(就是 Error: p3 foo),这是因为它需要在运行时消息队伍中添加处理程序,也就是说,在最终抛出未捕获错误之前它还会创建另一个 Promise。

这个例子同样揭示了异步错误有意思的副作用。正常情况下,在通过 throw 关键字抛出错误时,JS 运行时的错误处理机制会停止执行抛出错误之后的任何指令:

throw Error('foo');
console.log('bar'); // 这行不会执行
// Uncaught Error: foo

但是在 Promise 抛出错误时,因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时继续执行同步指令:

Promise.reject(Error('foo'));
console.log('bar');
// bar
// Uncaught (in promise) Error: foo

如本章前面的 Promise.reject() 示例,异步错误只能通过异步的 onRejected 出来才行捕获:

// 正确
Promise.reject(Error('foo')).catch(e => console.log(e));
// 错误
try {
  Promise.reject(Error('foo'));
} catch(e) {
}

这不包括捕获执行函数中的错误,在解决或拒绝 Promise 之前,仍然可以使用 try/catch 在执行函数中捕获错误:

let p = new Promise((res, rej) => {
  try {
    throw Error('foo');
  } catch(e) {
    res('bar');
  }
});
setTimeout(console.log, 0, p); // Promise <fulfilled>: bar

then() 和 catch() 的 onRejected 处理程序在语义上相当于 try/catch。出发点都是捕获错误之后将其隔离(2022年了看见这词情不自禁发抖),同时不影响正常逻辑执行。为此,onRejected 处理程序的任务应该是在捕获异步错误之后返回一个解决的 Promise。

console.log('begin synchronous execution');
try {
  throw Error('foo');
} catch(e) {
  console.log('caught error', e);
}
console.log('continue synchronous execution');
// begin synchronous execution
// caught error Error: foo
// continue synchronous execution

let p = new Promise((res, rej) => {
  console.log('begin asynchronous execution');
  reject(Error('bar'));
}).catch(e => {
  console.log('caught error', e);
}).then(() => {
  console.log('continue asynchronous execution');
});

// begin asynchronous execution
// caught error Error: bar
// continue asynchronous execution

setTimeout(console.log, 0, p);
// Promise <fulfilled>: undefined

Promise 连锁和 Promise 合成

多个 Promise 组合在一起可以构成强大的代码逻辑。这种组合可以通过两种方式实现:连锁与合成。前者是一个接一个地拼接,后者则是将多个组合为一个。

连锁

把 Promise 逐个地串联起来是一种非常有用的编程模式。之所以可以这样,是因为每个 Promise 实例的方法(then/catch/finally)都会返回一个新的 Promise,而这个新的家伙又有自己的实例方法。

let p = new Promise((res, rej) => {
  console.log(1);
  res();
});
p.then(() => console.log(2))
 .then(() => console.log(3))
 .then(() => console.log(4));
// 1
// 2
// 3
// 4

这个实现最终执行了一连串“同步”任务,正因如此,这样没啥用,毕竟分别使用4个同步函数也可以做到:

(() => console.log(1))(); 
(() => console.log(2))(); 
(() => console.log(3))(); 
(() => console.log(4))();

要真正执行异步任务,可以改写,让每个执行器都返回一个 Promise 实例。这样可以让每个后续 promise 都等待之前的 promise,也就是串行化异步任务。比如像下面让每个 promise 在一定时间后解决:

let p1 = new Promise((res, rej) => {
  console.log('p1');
  setTimeout(res, 1000);
});
p1.then(() => new Promise((res, rej) => {
  console.log('p2');
  setTimeout(res, 1000);
})).then(() => new Promise((res, rej) => {
  console.log('p3');
  setTimeout(res, 1000);
})).then(() => new Promise((res, rej) => {
  console.log('p4');
  setTimeout(res, 1000);
}));
// p1 
// (1秒后)p2
// (2秒后)p3
// (3秒后)p4

将生成 Promise 的代码提取到工厂函数中

function delayedResolve(str) {
  return new Promise((res, rej) => {
    console.log(str);
    setTimeout(res, 1000);
  });
}
delayedResolve('p1')
  .then(() => delayedResolve('p2'))
  .then(() => delayedResolve('p3'))
  .then(() => delayedResolve('p4'));
// p1 
// (1秒后)p2
// (2秒后)p3
// (3秒后)p4

每个 Promise 的处理程序都会等待前一个 Promise 解决,然后实例化一个新 Promise 并返回它,这种结构可以简化地将异步任务串行化,解决之前依赖回调的难题,假如不使用 promise,那么前面的代码可能就这样写了:

function delayedExecute(str, callback = null) {
  setTimeout(() => {
    console.log(str);
    callback && callback();
  }, 1000);
}
delayedExecute('p1', () => {
  delayedExecute('p2', () => {
    delayedExecute('p3', () => {
      delayedExecute('p4');
    });
  });
});
// 这里有些出入,因为执行器同步执行的
// (1秒后)p1
// (2秒后)p2
// (3秒后)p3
// (4秒后)p4

Promise 图

因为一个 Promise 可以有任意多个处理程序,所以连锁可以构建有向非循环图的结构。这样每个 promise 都是图中的一个节点,而使用实例方法添加的处理程序则是有向顶点。因为图中每个节点都会等待前一个节点落定,所以图的方向就是 promise 的解决或拒绝顺序。

// 下面的例子展示了一种 promise 有向图,也就是二叉树:
//     A 
//    / \ 
//   B   C 
//  /\   /\ 
//  D E  F G

let A = new Promise((res, rej) => {
  console.log('A');
  res();
});

let B = A.then(() => console.log('B'));
let C = A.then(() => console.log('C'));

B.then(() => console.log('D'));
B.then(() => console.log('E'));
C.then(() => console.log('F'));
C.then(() => console.log('G'));
// A
// B
// C
// D
// E
// F
// G

注意输出语句是对二叉树的层序遍历。promise 的处理程序是按照他们添加的顺序执行的。

由于 promise 的处理程序先添加到消息队列,然后才逐个执行,因此构成了层序遍历。树只是图的一种形式。考虑到根节点不一定唯一,且多个 promise 也可以组成一个 promise,所以有向非循环图是体现 promise 连锁可能性的最准确表达。

Promise.all 和 Promise.race

Promise 类提供两个将多个 promise 实例组合成一个 promise 的静态方法。而合成 promise 的行为取决于内部 promise 的行为。

Promise.all

该方法创建的 promise 会在一组 promise 全部解决之后再解决。接收一个可迭代对象,返回一新 promise:

let p1 = Promise.all([
  Promise.resolve(),
  Promise.resolve()
]);
// 可迭代对象中的元素会通过 Promise.resolve() 转换为 promise
let p2 = Promise.all([3, 4]);
// 空的可迭代对象等价于 Promise.resolve()
let p3 = Promise.all([]);

// 无效
let p4 = Promise.all();
// TypeError: : undefined is not iterable (cannot read property Symbol(Symbol.iterator))

合成的 promise 只会在每个包含的 promise 都解决之后才解决:

let p = Promise.all([
  Promise.resolve(),
  new Promise((res, rej) => setTimeout(res, 1000))
]);
setTimeout(console.log, 0, p); // Promise <pending>

p.then(() => setTimeout(console.log, 0, 'all() resolved'));

// all() resolved(大约1秒后)

如果至少有一个包含的 promise 待定,则合成的 promise 也会待定。如果有一个包含的 promise 拒绝,则合成的 promise 也会拒绝:

// 永远待定
let p1 = Promise.all([ new Promise(() => {}) ]);
setTimeout(console.log, 0, p1);

let p2 = Promise.all([
  Promise.resolve(),
  Promise.reject(),
  Promise.resolve(),
]);
setTimeout(console.log, 0, p2); // Promise <rejected>

// Uncaught (in promise) undefined

如果所有 promise 都成功解决,则合成 promise 的解决值就是所有包含 promise 解决值的数组,按照迭代顺序:

let p = Promise.all([
  Promise.resolve(3),
  Promise.resolve(),
  Promise.resolve(4),
]);
p.then(values => setTimeout(console.log, 0, values));
// [3, undefined, 4]

如果有 promise 拒绝,则第一个拒绝的 promise 会将自己的理由作为合成 promise 的拒绝理由。之后再拒绝的 promise 不会影响最终 promise 的拒绝理由。不过,这并不影响所有包含 promise 正常的拒绝操作。合成的 promise 会静默处理所有包含 promise 的拒绝操作。

let p = Promise.all([ 
 Promise.reject(3), 
 new Promise((resolve, reject) => setTimeout(reject, 1000)) 
]);
p.catch((reason) => setTimeout(console.log, 0, reason)); // 3

// 没有未处理的错误

Promise.race

该静态方法返回一个包装 promise,是一组集合中最先解决或拒绝的 promise 的镜像。这个方法接收一个可迭代对象,返回一个新 promise:

let p1 = Promise.race([
  Promise.resolve(),
  Promise.resolve()
]);
// 可迭代对象中的元素会通过 Promise.resolve 转换为 promise
let p2 = Promise.race([3, 4]);
// 空的可迭代对象等等价于 new Promise(() => {})
let p3 = Promise.race([]); // Promise <pending> 和 all 不一样(<fulfilled>)
// 无效
let p4 = Promise.race();
// TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))

Promise.race 不会对解决或拒绝的 promise 区别对待。无论是解决还是拒绝,只要是第一个落定的 promise,Promise.race 就会包装其解决值或拒绝理由并返回新 Promise:

// 解决先发生,超时后的拒绝被忽略
let p1 = Promise.race([
  Promise.resolve(),
  new Promise((res, rej) => setTimeout(rej, 1000))
]);
setTimeout(console.log, 0, p1); // Promise <fulfilled>: 3

// 拒绝先发生,超时后的解决被忽略
let p2 = Promise.race([
  Promise.reject(4),
  new Promise((res, rej) => setTimeout(res, 1000))
]);
setTimeout(console.log, 0, p2); // Promise <rejected>: 4

// 迭代顺序决定了落定顺序
let p3 = Promise.race([
  Promise.resolve(5),
  Promise.resolve(6),
  Promise.resolve(7),
]);
setTimeout(console.log, 0, p2); // Promise <fulfilled>: 5

如果有一个 promise 拒绝,只要它是第一个落定的,就会成为拒绝合成 promise 的理由。之后再拒绝的 promise 不会影响最终 promise 的拒绝理由。不过,这并不影响所有包含 promise 正常的拒绝操作。与 Promise.all() 类似,合成的 promise 会静默处理所有包含 promise 的拒绝操作。

let p = Promise.race([
  Promise.reject(3),
  new Promise((res, rej) => setTimeout(rej, 1000))
]);
p.catch(reason => setTimeout(console.log, 0, reason)); // 3

// 没有未处理的错误

串行 promise 合成

这很像函数合成,即将多个函数合称为一个函数:

function addTwo(x) { return x + 2 }
function addThree(x) { return x + 3 }
function addFive(x) { return x + 5 }
function addTen(x) {
  return addFive(addTwo(addThree(x)));
}
console.log(addTen(7)); // 17

类似地,promise 也可以像这样合成起来,渐进地消费一个值,并返回一个结果:

function addTen(x) {
  return Promise.resolve(x)
    .then(addTwo)
    .then(addThree)
    .then(addFive);
}
addTen(8).then(console.log); // 18

利用 Array.prototype.reduce 可以更简洁,关于 reduce 的使用

function addTen(x) {
  return [addTwo, addThree, addFive]
    .reduce((promise, fn) => promise.then(fn), Promise.resolve(x));
}
addTen(8).then(console.log); // 18

Promise 扩展

ES6 Promise 实现是很可靠,但也有不足之处,比如,很多第三方 promise 库实现中都具备而 ES6 规范未涉及的两个特性:取消和进度追踪。

Promise 取消

经常会遇到 promise 正在处理过程中,程序却不需要其结果的情形。这时候如果能够取消 promise 就好了,某些第三方库比如 Bluebird,就提供了这个特性。Kevin Smith 提到了“取消令牌”(cancel token),生成的令牌实例提供了一个接口,利用这个接口可以取消 promise;同时也提供了一个 promise 实例,可以用来触发取消后的操作并求值取消状态。

class CancelToken {
  constructor(cancelFn) {
    this.promise = new Promise((res, rej) => {
      cancelFn(res);
    });
  }
}

这个类包装了一个 promise,把解决方法暴露给了一个 cancelFn 参数。这样,外部代码就可以向构造函数中传入一个函数,从而控制什么情况下可以取消 promise。这里的 promise 是令牌类的公共成员,因此可以给它添加处理程序以取消 promise

<button id="start">start</button>
<button id="cancel">cancel</button>

<script>
    class CancelToken {
        constructor(cancelFn) {
            this.promise = new Promise((res, rej) => {
                cancelFn(() => {
                    setTimeout(console.log, 0, '延迟取消');
                    res();
                });
            })
        }
    }

    const sBtn = document.querySelector('#start');
    const cBtn = document.querySelector('#cancel');

    function cancelable(delay) {
        setTimeout(console.log, 0, '设置延迟');
        return new Promise((res, rej) => {
            const id = setTimeout(() => {
                setTimeout(console.log, 0, '被延迟的解决');
                res();
            }, delay);
            
            const cancelToken = new CancelToken(cancelCb => cBtn.addEventListener('click', cancelCb));

            cancelToken.promise.then(() => clearTimeout(id));
        })
    }

    sBtn.addEventListener('click', () => cancelable(2000));
</script>

每次单击“Start”按钮都会开始计时,并实例化一个新的 CancelToken 的实例。此时,“Cancel” 按钮一旦被点击,就会触发令牌实例中的期约解决。而解决之后,单击“Start”按钮设置的超时也会被取消。

Promise 进度通知

执行中的 promise 可能会有不少离散的“阶段”,在最终解决之前必须一次经过。某些情况下,监控 promise 进度会很有用。ES6 promise 不支持进度追踪,但可以通过扩展来实现。

方式一:扩展 Promise 类,为它添加 notify 方法

class TrackablePromise extends Promise {
  constructor(executor) {
    const notifyHandlers = [];
    super((res, rej) => {
      return executor(res, rej, (status) => {
        notifyHandlers.map(handler => handler(status));
      });
    });

    this.notifyHandlers = notifyHandlers;
  }

  notify(notifyHandler) {
    this.notifyHandlers.push(notifyHandlers);
    return this;
  }
}

这样,TrackablePromise 就可以在执行函数中使用 notify 函数了

let p = new TrackablePromise((res, rej, notify) => {
  function countdown(x) {
    if (x > 0) {
      notify(`${20 * x}% remaining`);
      setTimeout(() => countdown(x - 1), 1000);
    } else {
      res();
    }
  }

  countdown(5);
});

这个 promise 会连续 5 次递归地设置 1000 毫秒的超时,每个超时回调都会调用 notify 并传入状态值,假设通知处理程序简单地这样写:

p.notify((x) => setTimeout(console.log, 0, 'process:', x));

p.then(() => setTimeout(console.log, 0, 'completed'));

// (约 1 秒后)80% remaining 
// (约 2 秒后)60% remaining 
// (约 3 秒后)40% remaining 
// (约 4 秒后)20% remaining 
// (约 5 秒后)completed

notify 会返回 promise,所以可以连锁调用,连续添加处理程序。多个处理程序会针对收到的每条消息分别执行一遍,如下所示:

p.notify(x => setTimeout(console.log, 0, 'a:', x))
 .notify(x => setTimeout(console.log, 0, 'b:', x));

p.then(() => setTimeout(console.log, 0, 'completed'));

// (约 1 秒后) a: 80% remaining 
// (约 1 秒后) b: 80% remaining 
// (约 2 秒后) a: 60% remaining 
// (约 2 秒后) b: 60% remaining 
// (约 3 秒后) a: 40% remaining 
// (约 3 秒后) b: 40% remaining 
// (约 4 秒后) a: 20% remaining 
// (约 4 秒后) b: 20% remaining 
// (约 5 秒后) completed

总体来看,还是比较粗糙,但可以演示出如果使用通知报告进度了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容