异步函数 | Promise连锁和合成

期约连锁

因为每个Promise实例的方法(then() catch() finally())都可以返回一个Promise实例,新的Promise实例又有自己的实例方法,这样连缀的方法调用就可以构成所谓的“期约连锁“,也就是平时说的链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let p1 = new Promise((resolve, reject) => {
console.log('p1 executor');
setTimeout(resolve, 1000);
});

p1.then(() => new Promise((resolve, reject) => {
console.log('p2 executor');
setTimeout(resolve, 1000);
}))
.then(() => new Promise((resolve, reject) => {
console.log('p3 executor');
setTimeout(resolve, 1000);
}))
.then(() => new Promise((resolve, reject) => {
console.log('p4 executor');
setTimeout(resolve, 1000);
}));

// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)

把生成期约的代码提取到一个工厂函数中,就可以简洁代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function delayedResolved(str) {
return new Promise((resolve, reject) => {
console.log(str);
setTimeout(resolve, 1000);
})
}

delayedResolve('p1 executor')
.then(() => delayedResolve('p2 executor'))
.then(() => delayedResolve('p3 executor'))
.then(() => delayedResolve('p4 executor'))
// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)

期约连锁很好地解决了回调地狱的问题

Promise图

因为一个期约可以有任意多个处理程序,所以期约连锁可以构建有向非循环图的结构

这样,每个期约都是图中的一个节点,而使用实例方法添加的处理程序则是有向顶点

因为图中的每个节点都会等待前一个节点落定,所以图的方向就是期约的解决或拒绝顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// A
// / \
// B C
// /\ /\
// D E F G
let A = new Promise((resolve, reject) => {
console.log('A');
resolve();
});

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合成

(1)Promise.all()

手写原理 | Promise.all

Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决

  1. Promise.all()法接收一个可迭代对象,返回一个新Promise
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let p1 = Promise.all([
Promise.reslove(),
Promise.resolve()
]);

// 以下都是在手写的时候要注意的!!!!!!

// 可迭代对象中的元素会通过Promise.resolve()转换为期约
let p2 = Promise.all([3, 4]);
// 空的可迭代对象等价于Promise.resolve()
let p3 = Promise.all([]);
// 无效的语法
let p4 = Promise.all();
// TypeError: cannot read Symbol.iterator of undefined
  1. Promise.all只会在每个包含Promise都解决之后才会解决
1
2
3
4
5
6
7
8
let p = Promise.all([
Promise.resolve(),
new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);

setTimeout(console.log, 0, p); // Promise <pending>
p.then(() => setTimeout(console.log, 0, 'all() resolved!'));
// all() resolved!(大约1 秒后)
  1. 如果至少一个包含的Promise待定,则合成的Promise也会待定
1
2
3
// 永远待定
let p1 = Promise.all([new Promise(() => {})]);
setTimeout(console.log, 0, p1); // Promise <pending>
  1. 如果有一个包含的Promise拒绝,则最终合成的Promise也会拒绝
1
2
3
4
5
6
7
8
9
10
11
12
13
// 永远待定
let p1 = Promise.all([new Promise(() => {})]);
setTimeout(console.log, 0, p1); // Promise <pending>

// 一次拒绝会导致最终期约拒绝
let p2 = Promise.all([
Promise.resolve(),
Promise.reject(),
Promise.resolve()
]);

setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught (in promise) undefined
  1. 如果所有的Promise都解决,则合成的Promise的解决值就是所有Promise解决值的数组,顺序按照迭代顺序
1
2
3
4
5
6
7
let p = Promise.all([
Promise.resolve(3),
Promise.resolve(),
Promise.resolve(4)
]);

p.then((values) => setTimeout(console.log, 0, values)); // [3, undefined, 4]
  1. 如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由,之后再拒绝的期约不会影响最终期约的拒绝理由,不过,这并不影响所有包含期约正常的拒绝操作,合成的期约会静默处理所有包含期约的拒绝操作
1
2
3
4
5
6
7
8
9
10
// 虽然只有第一个期约的拒绝理由会进入
// 拒绝处理程序,第二个期约的拒绝也
// 会被静默处理,不会有错误跑掉
let p = Promise.all([
Promise.reject(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);

p.catch((reason) => setTimeout(console.log, 0, reason)); // 3
// 没有未处理的错误

(2)Promise.race()

Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像

  1. Promise.race()接收一个可迭代数组,返回一个新的Promise
1
2
3
4
5
6
7
8
9
10
11
12
let p1 = Promise.race([
Promise.resolve(),
Promise.resolve()
]);

// 可迭代对象中的元素会通过Promise.resolve()转换为期约
let p2 = Promise.race([3, 4]);
// 空的可迭代对象等价于new Promise(() => {})
let p3 = Promise.race([]);
// 无效的语法
let p4 = Promise.race();
// TypeError: cannot read Symbol.iterator of undefined
  1. Promise.race()不会对解决或拒绝的期约区别对待。无论是解决还是拒绝,只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 解决先发生,超时后的拒绝被忽略
let p1 = Promise.race([
Promise.resolve(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);

setTimeout(console.log, 0, p1); // Promise <resolved>: 3

// 拒绝先发生,超时后的解决被忽略
let p2 = Promise.race([
Promise.reject(4),
new Promise((resolve, reject) => setTimeout(resolve, 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, p3); // Promise <resolved>: 5
  1. 如果有一个期约拒绝,只要它是第一个落定的,就会成为拒绝合成期约的理由,之后再拒绝的期约不会影响最终期约的拒绝理由,不过,这并不影响所有包含期约正常的拒绝操作,与Promise.all()类似,合成的期约会静默处理所有包含期约的拒绝操作
1
2
3
4
5
6
7
8
9
10
// 虽然只有第一个期约的拒绝理由会进入
// 拒绝处理程序,第二个期约的拒绝也
// 会被静默处理,不会有错误跑掉
let p = Promise.race([
Promise.reject(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);

p.catch((reason) => setTimeout(console.log, 0, reason)); // 3
// 没有未处理的错误

串行Promise合成

Promise的一个主要特性是:异步产生值并将其传给处理程序,基于后续期约使用之前期约的返回值来串联期约是期约的基本功能

参考函数合成,我们也可以将Promise进行合成

1
2
3
4
5
6
7
8
9
10
11
function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}
function addTen(x) {
return Promise.resolve(x)
.then(addTwo)
.then(addThree)
.then(addFive);
}

addTen(8).then(console.log); // 18
  • reduce改进
1
2
3
4
5
6
7
function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}

function addTen(x) {
return [addTwo, addThree, addFive].reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}
  • 提炼出通用函数
1
2
3
4
5
6
7
8
9
10
11
function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}

function compose(...fns) {
return (x) => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}

let addTen = compose(addTwo, addThree, addFive);

addTen(8).then(console.log); // 18

知识补充

手写原理 | Promise.all

手写原理 | Promise.race

手写原理 | Promise.allSelected

参考

Promises/A+