# 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 毫秒之后)
1
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
  });
1
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 
1
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
1
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 ]
1
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 
1
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
1
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
1
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() {} 
} 
1
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);
1
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 
1
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 
1
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]);
} 
1
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 
1
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
1
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();
1
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 
1
2
3
4
5
6
7
8
9
10
上次更新: 3/9/2022, 6:56:39 PM