# 异步操作

# Promise

# 简介

Promise 是异步编程的一种解决方案,比传统的回调函数更加合理,所谓 Promise,简单来说就是一个容器,里面保存着某个未来才会结束的事件。

Promise 对象的状态不受外界改变,这也是其英语含义的意思”承诺“,表示其他手段无法改变,只有异步操作的结果才能决定当前的状态。

Promise 对象有三种状态,分别是 pending,fulfilled,reject

Promise 对象的状态一旦改变就不会再发生变化,如果改变发生了,那么对这个 Promise 对象添加回调函数,会立即得到结果,而不是像事件一样,错过就没了。

Promise 的设计思想是,让所有的异步任务都返回一个 Promise 实例对象,Promise 实例有一个 then()方法,用来指定下一步的回调函数。

# Promise 构造函数

​ JavaScript 提供元素的 Promise 构造函数,用来生成 Promise 实例,Promise 构造函接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject,前者负责将 Promise 的状态变为成功,并将异步操作的结果作为参数传递出去,后者负责将 Promise 的状态变为失败,并将异步操作报出的错误,作为参数传递出去。

let promise = new Promise(function (resolve, reject) {
  // executor(生产者代码,“歌手”)
});

resolve/reject 可以立即进行

实际上,executor 通常是异步执行某些操作,并在一段时间后调用 resolve/reject,但这不是必须的。我们还可以立即调用 resolve 或 reject,就像这样:

let promise = new Promise(function (resolve, reject) {
  // 不花时间去做这项工作
  resolve(123); // 立即给出结果:123
});

# Promise 对象的状态

  1. Promise 对象通过自身的状态来控制异步操作,它具有三种状态 : 未完成 pending、成功 fulfilled、失败 rejected,其中成功和失败合在一起成为已定型状态 resolved,异步操作的状态从“开始执行”到“未完成“到成功或者失败,一旦状态发生变化到成功或者失败,状态就凝固了,不会再有新的状态变化。
  2. 状态凝固也是 Promise 这个名字的由来,它的英文意思是“承诺”,一旦承诺生效,就不得再改变了。这也也为这,Promise 实例的状态变化只能发生一次。
  3. 因此,Promise 的最终结果只有两种:异步操作成功,Promise 传出一个 value,状态变为 fulfilled,被 then()方法捕获;异步操作失败,Promise 抛出一个 error,状态变为 rejected,被 catch()方法捕获。

state 和 result 都是内部的

Promise 对象的 state 和 result 属性都是内部的。我们无法直接访问它们。
但我们可以对它们使用 .then/.catch/.finally 方法。我们在下面对这些方法进行了描述。

# Promised 对象的使用

then

最重要最基础的一个就是 .then

// Promise 的写法
let promise = new Promise(step1);
promise.then(step2).then(step3).then(step4);

catch

如果我们只对 error 感兴趣,那么我们可以使用 null 作为第一个参数:.then(null, errorHandlingFunction)。或者我们也可以使用 .catch(errorHandlingFunction),其实是一样的

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// .catch(f) 与 promise.then(null, f) 一样
promise.catch(alert); // 1 秒后显示 "Error: Whoops!"

finally

就像常规 try {...} catch {...} 中的 finally 子句一样,promise 中也有 finally。

.finally(f) 调用与 .then(f, f) 类似,在某种意义上,f 总是在 promise 被 settled 时运行:即 promise 被 resolve 或 reject。

new Promise((resolve, reject) => {
  /* 做一些需要时间的事儿,然后调用 resolve/reject */
})
  // 在 promise 为 settled 时运行,无论成功与否
  .finally(() => stop loading indicator)
  // 所以,加载指示器(loading indicator)始终会在我们处理结果/错误之前停止
  .then(result => show result, err => show error)

# 实例:图片加载

var preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    var image = new Image();
    image.onload = resolve;
    image.onerror = reject;
    image.src = path;
  });
};
preloadImage("https://example.com/my.jpg")
  .then(function (e) {
    document.body.append(e.target);
  })
  .then(function () {
    console.log("加载成功");
  });

上面代码中,image 是一个图片对象的实例,它有两个事件监听属性,分别对应加载成功和失败。图片加载成功后,onload 属性会返回一个事件对象,第一个 then()方法的回调函数会接受到这个事件对象,该对象的 target 属性就是图片加载后生成的 DOM 节点,也就是事件源。然后第二个 then()接受到 DOM 节点插入成功后,调用回调函数,打印出“加载成功”。

# 总结

Promise 的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。而且 Promise 还有一个传统写法没有的好处:它的状态一旦发生改变,无论何时查询,都能得到这个状态。

Promise 的缺点是,编写的难度比传统写法高,而且必须在自己的 then()的回调函数里面理清逻辑。

如果 .then(或 catch/finally 都可以)处理程序(handler)返回一个 promise,那么链的其余部分将会等待,直到它状态变为 settled。当它被 settled 后,其 result(或 error)将被进一步传递下去。

# Promise API

在 Promise 类中,有 6 种静态方法。我们在这里简单介绍下它们的使用场景。

# Promise.all

假设我们希望并行执行多个 promise,并等待所有 promise 都准备就绪。

例如,并行下载几个 URL,并等到所有内容都下载完毕后再对它们进行处理。

这就是 Promise.all 的用途。

Promise.all 接受一个 promise 数组作为参数(从技术上讲,它可以是任何可迭代的,但通常是一个数组)并返回一个新的 promise。

当所有给定的 promise 都被 settled 时,新的 promise 才会 resolve,并且其结果数组将成为新的 promise 的结果。

Promise.all([
  new Promise((resolve) => setTimeout(() => resolve(1), 3000)), // 1
  new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2
  new Promise((resolve) => setTimeout(() => resolve(3), 1000)) // 3
]).then(alert); // 1,2,3 当上面这些 promise 准备好时:每个 promise 都贡献了数组中的一个元素

如果出现 error,其他 promise 将被忽略

如果其中一个 promise 被 reject,Promise.all 就会立即被 reject,完全忽略列表中其他的 promise。它们的结果也被忽略。

# Promise.allSettled

如果任意的 promise reject,则 Promise.all 整个将会 reject。当我们需要 所有 结果都成功时,它对这种“全有或全无”的情况很有用。

但是我们不要求全部成功时,就不好处理了,这时我们可以使用Promise.allSettledPromise.allSettled 等待所有的 promise 都被 settle,无论结果如何

例如,我们想要获取(fetch)多个用户的信息。即使其中一个请求失败,我们仍然对其他的感兴趣。

let urls = [
  "https://api.github.com/users/iliakan",
  "https://api.github.com/users/remy",
  "https://no-such-url" // 预期这个会失败
];

Promise.allSettled(urls.map((url) => fetch(url))).then((results) => {
  // (*)
  results.forEach((result, num) => {
    if (result.status == "fulfilled") {
      alert(`${urls[num]}: ${result.value.status}`);
    }
    if (result.status == "rejected") {
      alert(`${urls[num]}: ${result.reason}`);
    }
  });
});

返回结果

[
  {status: 'fulfilled', value: ...response...},
  {status: 'fulfilled', value: ...response...},
  {status: 'rejected', reason: ...error object...}
]

# Promise.any

Promise.any() 接收一个 Promise 可迭代对象,只要其中的一个 promise 成功,就返回那个已经成功的 promise。

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 3

# Promise.race

如果我们不关心是否成功与失败

Promise.race() 接收一个 Promise 可迭代对象,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => reject(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

# Promise.resove

# Promise.reject

# 总结

  • Promi1se.all(promises) —— 等待所有 promise 都 resolve 时,返回存放它们结果的数组。如果给定的任意一个 promise 为 reject,那么它就会变成 Promise.all 的 error,所有其他 promise 的结果都会被忽略。
  • Promise.allSettled(promises)(ES2020 新增方法)—— 等待所有 promise 都 settle 时,并以包含以下内容的对象数组的形式返回它们的结果:
  • Promise.any(promises) —— 等待第一个 fulfilled 的 promise,并将其 result/error 作为结果。
  • Promise.race(promises) —— 等待第一个 settle 的 promise,并将其 result/error 作为结果。

选择:

  1. 全部成功选all
  2. 全部完成不关心成功与否选allSettled
  3. 有一个成功即可选any
  4. 有一个完成即可选race

# 微任务

Promise 的处理程序(handlers).then、.catch 和 .finally 都是异步的。

即便一个 promise 立即被 resolve,.then、.catch 和 .finally 下面 的代码也会在这些处理程序(handler)之前被执行。

示例代码如下:

let promise = Promise.resolve();

promise.then(() => alert("promise done!"));

alert("code finished"); // 这个 alert 先显示

# 微任务队列

异步任务需要适当的管理。为此,ECMA 标准规定了一个内部队列 PromiseJobs,通常被称为“微任务队列(microtask queue)”。如 规范 中所述:

  • 队列(queue)是先进先出的:首先进入队列的任务会首先运行。
  • 只有在 JavaScript 引擎中没有其它任务在运行时,才开始执行任务队列中的任务。

如果执行顺序对我们很重要该怎么办?我们怎么才能让 code finished 在 promise done 之后运行呢? 很简单,只需要像下面这样使用 .then 将其放入队列:

Promise.resolve()
  .then(() => alert("promise done!"))
  .then(() => alert("code finished"));

# 总结

Promise 处理始终是异步的,因为所有 promise 行为都会通过内部的 “promise jobs” 队列,也被称为“微任务队列”(ES8 术语)。

因此,.then/catch/finally 处理程序(handler)总是在当前代码完成后才会被调用。

# Async / Await

Async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。

# Async Function

在函数前面的 “async” 这个单词表达了一个简单的事情:即这个函数总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中。

async function f() {
  return 1;
}

f().then(alert); // 1

async 确保了函数返回一个 promise,也会将非 promise 的值包装进去。很简单,对吧?但不仅仅这些。还有另外一个叫 await 的关键词,它只在 async 函数内工作,也非常酷。

# Await

关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。

async function f() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 1000);
  });

  let result = await promise; // 等待,直到 promise resolve (*)

  alert(result); // "done!"
}

f();

await 不会阻塞 JS 线程

让我们强调一下:await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。

关于 await 还需要注意的几点

不能在普通函数中使用 await, 如果我们尝试在非 async 函数中使用 await 的话,就会报语法错误。 await 不能在顶层代码运行,P.S. 新特性:从 V8 引擎 8.9+ 版本开始,顶层 await 可以在 模块 中工作。

# 错误处理

如果一个 promise 正常 resolve,await promise 返回的就是其结果。但是如果 promise 被 reject,它将 throw 这个 error,就像在这一行有一个 throw 语句那样。

async function f() {
  await Promise.reject(new Error("Whoops!"));
}

和下面是一样的:

async function f() {
  throw new Error("Whoops!");
}

我们可以用 try..catch 来捕获上面提到的那个 error,与常规的 throw 使用的是一样的方式:

async function f() {
  try {
    let response = await fetch("http://no-such-url");
  } catch (err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

# 总结

函数前面的关键字 async 有两个作用:

  • 让这个函数总是返回一个 promise。
  • 允许在该函数内使用 await。

Promise 前的关键字 await 使 JavaScript 引擎等待该 promise settle,然后:

  • 如果有 error,就会抛出异常 — 就像那里调用了 throw error 一样。
  • 否则,就返回结果。