# 异步操作
# 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 对象的状态
- Promise 对象通过自身的状态来控制异步操作,它具有三种状态 : 未完成 pending、成功 fulfilled、失败 rejected,其中成功和失败合在一起成为已定型状态 resolved,异步操作的状态从“开始执行”到“未完成“到成功或者失败,一旦状态发生变化到成功或者失败,状态就凝固了,不会再有新的状态变化。
- 状态凝固也是 Promise 这个名字的由来,它的英文意思是“承诺”,一旦承诺生效,就不得再改变了。这也也为这,Promise 实例的状态变化只能发生一次。
- 因此,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.allSettled
,Promise.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 作为结果。
选择:
- 全部成功选
all
- 全部完成不关心成功与否选
allSettled
- 有一个成功即可选
any
- 有一个完成即可选
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 一样。
- 否则,就返回结果。