# Promise与异步函数
ES 6 新增了正式的 Promise( Promise)引用类型,支持优雅地定义和组织异步逻辑。
ES7 增加了使用 async 和 await 关键字定义异步函数的机制
# 1、异步编程
同步行为和异步行为的对立统一是计算机科学的一个基本概念。异步行为是为了优化因计算量大而时间长的操作。如果在等待其他操作完成的同时,即使运行其他指令,系统也能保持稳定。
function double(value, callback) {
setTimeout(() => callback(value * 2), 1000);
}
double(3, (x) => console.log(`I was given: ${x}`));
// I was given: 6(大约 1000 毫秒之后)
2
3
4
5
随着代码越来越复杂,回调策略是不具有扩展性的。“回调地狱”这个称呼可谓名至实归。嵌套回调的代码维护起来就是噩梦。
# 2、Promise
Promise 是异步编程的一种解决方案,可以替代传统的解决方案--回调函数和事件。ES6 统一了用法,并原生提供了 Promise 对象。
两个特点:
- (1)对象的状态不受外界影响。
- (2)一旦状态改变了就不会在变,也就是说任何时候 Promise 都只有一种状态。
Promise 是一个有状态的对象,可能处于如下 3 种状态之一:
- 待定(pending)
- 兑现(fulfilled,有时候也称为“解决”,resolved)
- 拒绝(rejected)
待定(pending)是 Promise 的最初始状态。在待定状态下,Promise 可以落定(settled)为代表成功的兑现(fulfilled)状态,或者代表失败的拒绝(rejected)状态。无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝,Promise 的状态就不再改变。
function promises(a, b) {
return new Promise((resolve, reject) => {
if (a > b) {
cons
resolve(a - b);
} else {
reject(a + b);
}
});
}
promises(10, 7)
.then((res) => {
console.log(res); // 3
return promises(4, 6);
})
.catch((res) => {
console.log(res); // 10
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(1)Promise.all()
Promise.all() 静态方法创建的 Promise 会在一组 Promise 全部解决之后再解决。这个静态方法接收一个可迭代对象,返回一个新 Promise
Promise.all() 是并发运行,即同时允许多个函数
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: cannot read Symbol.iterator of undefined
2
3
4
5
6
7
8
9
10
11
12
13
14
如果至少有一个包含的 Promise 待定,则合成的 Promise 也会待定。如果有一个包含的 Promise 拒绝,则合成的 Promise 也会拒绝:
// 一次拒绝会导致最终 Promise 拒绝
let p = Promise.all([
Promise.resolve(1),
Promise.reject(2),
Promise.resolve(3),
]);
p.then(
(res) => {
console.log(res);
},
(rej) => {
console.log(rej);
}
);
// 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如果所有 Promise 都成功解决,则合成 Promise 的解决值就是所有包含 Promise 解决值的数组,按照迭代器顺序:
let p = Promise.all([
Promise.resolve(3),
Promise.resolve(),
Promise.resolve(4),
]);
p.then((res) => {
console.log(res);
});
// [ 3, undefined, 4 ]
2
3
4
5
6
7
8
9
10
(2)Promise.race()
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([]);
// 无效的语法
let p4 = Promise.race();
// TypeError: cannot read Symbol.iterator of undefined
2
3
4
5
6
7
8
9
10
11
12
13
14
Promise.race() 不会对解决或拒绝的 Promise 区别对待。无论是解决还是拒绝,只要是第一个落定的 Promise,Promise.race() 就会包装其解决值或拒绝理由并返回新 Promise:
// 迭代顺序决定了落定顺序
let p = Promise.race([
Promise.resolve(5),
Promise.reject(6),
Promise.resolve(7),
]);
p.then(
(res) => {
console.log(res);
},
(rej) => {
console.log(rej);
}
);
// 5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(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
// 使用 reduce
function addTen(x) {
return [addTwo, addThree, addFive]
.reduce((promise, fn) => promise.then(fn), Promise.resolve(x));
}
addTen(8).then(console.log); // 18
// 提取方法
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(4)Promise 扩展
ES6 Promise 实现是很可靠的,但它也有不足之处。Promise 规范却未涉及的两个特性:Promise 取消和进度追踪。
# 3、异步函数
异步函数,也称为“async/await”(语法关键字),是 ES6 Promise 模式在 JavaScript 函数中的应用。
# 3.1 异步函数
(1)async
async 关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上:
async function foo() {}
let bar = async function() {};
let baz = async () => {};
class Qux {
async qux() {}
}
2
3
4
5
6
使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。
不过,异步函数如果使用 return 关键字返回了值(如果没有 return 则会返回 undefined),这个值会被 Promise.resolve() 包装成一个 Promise 对象。异步函数始终返回 Promise 对象。
在函数外部调用这个函数可以得到它返回的 Promise:
async function foo() {
console.log(1);
return 3;
}
// 给返回的 Promise添加一个解决处理程序
foo().then(console.log);
console.log(2);
// 直接返回一个 Promise对象也是一样的
async function foo() {
console.log(1);
return Promise.resolve(3);
}
// 给返回的 Promise添加一个解决处理程序
foo().then(console.log);
console.log(2);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(2)await
因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用 await 关键字可以暂停异步函数代码的执行,等待 Promise解决。
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
p.then((x) => console.log(x)); // 3
// 使用 async/await 可以写成这样:
async function foo() {
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
console.log(await p);
}
foo();
// 3
2
3
4
5
6
7
8
9
10
注意,await 关键字会暂停执行异步函数后面的代码,让出 JavaScript 运行时的执行线程。这个行为与生成器函数中的 yield 关键字是一样的。await 关键字同样是尝试“解包”对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行。
(3)await 的限制
await 关键字必须在异步函数中使用,不能在顶级上下文如<script>
标签或模块中使用。不过,定义并立即调用异步函数是没问题的。
async function foo() {
console.log(await Promise.resolve(3));
}
foo();
// 3
// 立即调用的异步函数表达式
(async function() {
console.log(await Promise.resolve(3));
})();
// 3
2
3
4
5
6
7
8
9
10
(3)执行顺序
// 顺序执行
async function foo() {
await Promise.resolve(1);
await Promise.resolve(2);
}
// 并发运行
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(1);
async function foo() {
await Promise.all([p1, p2]);
}
2
3
4
5
6
7
8
9
10
11
12
# 3.2 停止和恢复执行
async function foo() {
console.log(await Promise.resolve('foo'));
}
async function bar() {
console.log(await 'bar');
}
async function baz() {
console.log('baz');
}
foo();
bar();
baz();
// baz
// bar
// foo
2
3
4
5
6
7
8
9
10
11
12
13
14
15
异步函数如果不包含 await 关键字,其执行基本上跟普通函数没有什么区别
要完全理解 await 关键字,必须知道它并非只是等待一个值可用那么简单。JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
# 3.3 异步函数策略
(1)实现 sleep()
function sleep(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
async function foo() {
console.log(111);
await sleep(1500); // 暂停约 1500 毫秒
console.log(222);
}
foo();
// 111
// 暂停约 1500 毫秒之后
// 222
2
3
4
5
6
7
8
9
10
11
12
13
14
(2)利用平行执行
如果使用 await 时不留心,则很可能错过平行加速的机会。
async function randomDelay(id) {
// 延迟 0~1000 毫秒
const delay = Math.random() * 1000;
return new Promise((resolve) => setTimeout(() => {
console.log(`${id} finished`);
resolve();
}, delay));
}
async function foo() {
const t0 = Date.now();
for (let i = 0; i < 5; ++i) {
await randomDelay(i);
}
console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 0 finished
// 1 finished
// 2 finished
// 3 finished
// 4 finished
// 877ms elapsed
// 修改后
async function foo() {
const t0 = Date.now();
const p0 = randomDelay(0);
const p1 = randomDelay(1);
const p2 = randomDelay(2);
const p3 = randomDelay(3);
const p4 = randomDelay(4);
await p0;
await p1;
await p2;
await p3;
await p4;
setTimeout(console.log, 0, `${Date.now() - t0}ms elapsed`);
}
foo();
async function foo() {
const t0 = Date.now();
const promises = Array(5).fill(null).map((_, i) => randomDelay(i));
for (const p of promises) {
await p;
}
console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
(3)串行执行 Promise
function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}
async function addTen(x) {
for (const fn of [addTwo, addThree, addFive]) {
x = await fn(x);
}
return x;
}
addTen(9).then(console.log); // 19
2
3
4
5
6
7
8
9
10