目录
其实我只是把MDN上的内容复制过来了而已。
同步编程
JavaScript 本质上是一门单线程语言。
观察下面的代码:
const name = '张三';
const msg = `Hello, my name is ${name}.`;
console.log(msg); // Hello, my name is 张三.
这段代码:
- 声明一个叫
name
的字符串常量 - 声明了另一个叫做
msg
的字符串常量(并使用了name
常量的值) - 将
msg
常量输出到 JavaScript 控制台中
我们应该注意的是,实际上浏览器是按照我们书写代码的顺序一行一行地执行程序的。浏览器会等待代码的解析和工作,在上一行完成后才会执行下一行。这样做是很有必要的,因为每一行新的代码都是建立在前面代码的基础之上的。
这也使得它成为一个同步程序。
事实上,调用函数的时候也是同步的,就像这样:
function getMag(name: any) {
return `Hello, my name is ${name}.`;
}
const name = '张三';
const msg = getMag(name);
console.log(msg); // Hello, my name is 张三.
在这里 getMag()
就是一个同步函数,因为在函数返回之前,调用者必须等待函数完成其工作。
一个耗时的同步函数
由于你的代码和浏览器的用户界面运行在同一个线程中,共享同一个事件循环,假如你的代码阻塞了或者进入了无限循环,则浏览器将会卡死。无论是由于 bug 引起还是代码中进行复杂的运算导致的性能降低,都会降低用户的体验。
如果同步函数需要很长的时间怎么办?
当用户点击“生成素数”按钮时,这个程序将使用一种非常低效的算法生成一些大素数。你可以控制要生成的素数数量,这也会影响操作需要的时间。
使用Quasar CLI搭建的vue3项目结构
quasar+vue3+ts
template
<template>
<div class="fit">
<div class="q-mb-md">async await return</div>
<div class="content q-gutter-md">
<q-card class="my-card bg-primary text-white">
<q-card-section>
<div class="q-mb-md q-gutter-sm">
<q-btn
color="white"
text-color="primary"
label="生成素数"
@click="generatePrimes"
/>
</div>
<div>
<q-input
dark
filled
label-color="white"
v-model="primeNum"
label="素数个数"
/>
</div>
<div>{{ msg }}</div>
</q-card-section>
</q-card>
</div>
</div>
</template>
script
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const primeNum = ref(1000000);
const msg = ref('');
onMounted(() => {
fun();
});
function getMag(name: any) {
return `Hello, my name is ${name}.`;
}
function fun() {
const name = '张三';
const msg = getMag(name);
console.log(msg); // Hello, my name is 张三.
}
// 生成素数
function generatePrimes() {
function isPrime(n: any) {
// Math.sqrt() 函数返回一个数的平方根
for (let c = 2; c <= Math.sqrt(n); ++c) {
if (n % c === 0) {
return false;
}
}
return true;
}
let primes: number[] = [];
const maximum = 1000000;
while (primes.length < primeNum.value) {
const candidate = Math.floor(Math.random() * (maximum + 1));
if (isPrime(candidate)) {
primes.push(candidate);
}
}
msg.value = `已生成素数${primes.length}个`;
}
</script>
style
<style lang="scss" scoped>
.content {
display: flex;
flex-flow: row wrap;
align-items: flex-start;
}
.my-card {
width: 300px;
}
</style>
你会发现,当我们的 generatePrimes()
函数运行时,我们的程序完全没有反应:用户不能输入任何东西,也不能点击任何东西,或做任何其他事情。
这就是耗时的同步函数的基本问题。在这里我们想要的是一种方法,以让我们的程序可以:
- 通过调用一个函数来启动一个长期运行的操作
- 让函数开始操作并立即返回,这样我们的程序就可以保持对其他事件做出反应的能力
- 当操作最终完成时,通知我们操作的结果。
这就是异步函数为我们提供的能力。
事件处理程序
我们刚才看到的对异步函数的描述可能会让你想起事件处理程序,这么想是对的。事件处理程序实际上就是异步编程的一种形式:你提供的函数(事件处理程序)将在事件发生时被调用(而不是立即被调用)。如果“事件”是“异步操作已经完成”,那么你就可以看到事件如何被用来通知调用者异步函数调用的结果的。
一些早期的异步 API 正是以这种方式来使用事件的。XMLHttpRequest
API 可以让你用 JavaScript 向远程服务器发起 HTTP 请求。由于这样的操作可能需要很长的时间,所以它被设计成异步 API,你可以通过给XMLHttpRequest
对象附加事件监听器来让程序在请求进展和最终完成时获得通知。
下面的例子展示了这样的操作。点击“点击发起请求”按钮来发送一个请求。我们将创建一个新的XMLHttpRequest
并监听它的loadend
事件。而我们的事件处理程序则会在控制台中输出一个“完成!”的消息和请求的状态代码。
我们在添加了事件监听器后发送请求。注意,在这之后,我们仍然可以在控制台中输出“请求已发起”,也就是说,我们的程序可以在请求进行的同时继续运行,而我们的事件处理程序将在请求完成时被调用。
template
<div class="q-mb-md q-gutter-sm">
<q-btn
color="white"
text-color="orange-4"
label="点击发起请求"
@click="xhrClick"
/>
<q-btn outline text-color="white" label="重载" @click="heavyLoad" />
</div>
<div>{{ requestMsg }}</div>
script
<script setup lang="ts">
import { ref } from 'vue';
const requestMsg = ref('');
function xhrClick() {
requestMsg.value = '';
const xhr = new XMLHttpRequest();
xhr.addEventListener('loadend', () => {
requestMsg.value = `${requestMsg.value}完成!状态码:${xhr.status}`;
});
xhr.open(
'GET',
'https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json'
);
xhr.send();
requestMsg.value = '请求已发起\n';
}
function heavyLoad() {
document.location.reload();
}
</script>
这就像我们在以前的模块中遇到的事件处理程序,只是这次的事件不是像点击按钮那样的用户行为,而是某个对象的状态变化。
然而,当回调函数本身需要调用其他同样接受回调函数的函数时,基于回调的代码会变得难以理解。当你需要执行一些分解成一系列异步函数的操作时,这将变得十分常见。例如下面这种情况:
异步
<script>
function fn1() {
console.log("fn1已执行");
setTimeout(() => {
console.log("异步操作已完成");
}, 1000);
}
function fn2() {
console.log("fn2已执行");
}
fn1()
fn2()
</script>
回调
一个函数作为参数被调用,这个函数就是回调函数。
回调函数不会被立即执行,它会在包含它的函数内部的某个特定时间被回调。
function fn(item, callback) {
let arr = [];
arr.push(item);
callback(arr);
}
// 匿名函数作为回调函数
fn('hello', function (arg) {
console.log(arg); // ['hello']
});
// 命名函数作为回调函数
function fun1(par) {
console.log(par); // ['你好']
}
fn('你好', fun1);
事件处理程序是一种特殊类型的回调函数。正如我们刚刚所看到的:回调函数曾经是 JavaScript 中实现异步函数的主要方式。
function doStep1(init) {
return init + 1;
}
function doStep2(init) {
return init + 2;
}
function doStep3(init) {
return init + 3;
}
function doOperation() {
let result = 0;
result = doStep1(result);
result = doStep2(result);
result = doStep3(result);
console.log(`结果:${result}`);
}
doOperation();
现在我们有一个被分成三步的操作,每一步都依赖于上一步。在这个例子中,第一步给输入的数据加 1,第二步加 2,第三步加 3。从输入 0 开始,最终结果是 6(0+1+2+3)。作为同步代码,这很容易理解。但是如果我们用回调来实现这些步骤呢?
function doStep1(init, callback) {
const result = init + 1;
callback(result);
}
function doStep2(init, callback) {
const result = init + 2;
callback(result);
}
function doStep3(init, callback) {
const result = init + 3;
callback(result);
}
function doOperation() {
doStep1(0, result1 => {
doStep2(result1, result2 => {
doStep3(result2, result3 => {
console.log(`结果:${result3}`);
});
});
});
}
doOperation();
因为必须在回调函数中调用回调函数,我们就得到了这个深度嵌套的doOperation()
函数,这就更难阅读和调试了。在一些地方这被称为“回调地狱”或“厄运金字塔”(因为缩进看起来像一个金字塔的侧面)。
面对这样的嵌套回调,处理错误也会变得非常困难:你必须在“金字塔”的每一级处理错误,而不是在最高一级一次完成错误处理。
由于以上这些原因,大多数现代异步 API 都不使用回调。事实上,JavaScript 中异步编程的基础是Promise
,这也是我们下一篇文章要讲述的主题。
如何使用 Promise
Promise 是现代 JavaScript 中异步编程的基础,是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象。在 Promise 返回给调用者的时候,操作往往还没有完成,但 Promise 对象可以让我们操作最终完成时对其进行处理(无论成功还是失败)。
在上一篇文章中,我们谈到使用回调实现异步函数的方法。在这种设计中,我们需要在调用异步函数的同时传入回调函数。这个异步函数会立即返回,并在操作完成后调用传入的回调。
在基于 Promise 的 API 中,异步函数会启动操作并返回 Promise 对象。然后,你可以将处理函数附加到 Promise 对象上,当操作完成时(成功或失败),这些处理函数将被执行。
使用 fetch() API
在这个例子中,我们将从https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json下载 JSON 文件,并记录一些相关信息。
要做到这一点,我们将向服务器发出一个 HTTP 请求。在 HTTP 请求中,我们向远程服务器发送一个请求信息,然后它向我们发送一个响应。这次,我们将发送一个请求,从服务器上获得一个 JSON 文件。还记得在上一篇文章中,我们使用 XMLHttpRequest
API 进行 HTTP 请求吗?那么,在这篇文章中,我们将使用 fetch()
API,一个现代的、基于 Promise 的、用于替代 XMLHttpRequest 的方法。
const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
console.log(fetchPromise);
fetchPromise.then( response => {
console.log(`已收到响应:${response.status}`);
});
console.log("已发送请求……");
我们在这里:
- 调用
fetch()
API,并将返回值赋给fetchPromise
变量。 - 紧接着,输出
fetchPromise
变量,输出结果应该像这样:Promise { <state>: "pending" }
。这告诉我们有一个Promise
对象,它有一个state
属性,值是"pending"
。"pending"
状态意味着操作仍在进行中。 - 将一个处理函数传递给
Promise
的then()
方法。当(如果)获取操作成功时,Promise
将调用我们的处理函数,传入一个包含服务器的响应的Response
对象。 - 输出一条信息,说明我们已经发送了这个请求。
完整的输出结果是这样的:
请注意,已发送请求……
的消息在我们收到响应之前就被输出了。与同步函数不同,fetch() 在请求仍在进行时返回,这使我们的程序能够保持响应性。响应显示了 200(OK)的状态码,意味着我们的请求成功了。
可能这看起来很像上面文章中的例子中我们把事件处理程序添加到 XMLHttpRequest
对象中。但不同的是,我们这一次将处理程序传递到返回的 Promise
对象的 then()
方法中。
链式使用 Promise
在你通过 fetch()
API 得到一个 Response
对象的时候,你需要调用另一个函数来获取响应数据。这次,我们想获得 JSON 格式的响应数据,所以我们会调用 Response 对象的 json()
方法。事实上,json() 也是异步的,因此我们必须连续调用两个异步函数。
const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
fetchPromise.then( response => {
const jsonPromise = response.json();
jsonPromise.then( json => {
console.log(json[0].name); // baked beans
});
});
在这个示例中,就像我们之前做的那样,我们给 fetch()
返回的 Promise 对象添加了一个 then()
处理程序。但这次我们的处理程序调用 response.json()
方法,然后将一个新的 then()
处理程序传递到 response.json()
返回的 Promise 中。
执行代码后应该会输出“baked beans”(“products.json”中第一个产品的名称)。
等等!还记得上一篇文章吗?我们好像说过,在回调中调用另一个回调会出现多层嵌套的情况?我们是不是还说过,这种“回调地狱”使我们的代码难以理解?这不是也一样吗,只不过变成了用 then()
调用而已?
当然如此。但 Promise 的优雅之处在于 then()
本身也会返回一个 Promise,这个 Promise 将指示 then()
中调用的异步函数的完成状态。这意味着我们可以(当然也应该)把上面的代码改写成这样:
const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
fetchPromise
.then( response => {
return response.json();
})
.then( json => {
console.log(json[0].name);
});
不必在第一个 then()
的处理程序中调用第二个 then()
,我们可以直接返回 json()
返回的 Promise,并在该返回值上调用第二个 “then()”。这被称为 Promise 链,意味着当我们需要连续进行异步函数调用时,我们就可以避免不断嵌套带来的缩进增加。
在进入下一步之前,还有一件事要补充:我们需要在尝试读取请求之前检查服务器是否接受并处理了该请求。我们将通过检查响应中的状态码来做到这一点,如果状态码不是“OK”,就抛出一个错误:
const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
fetchPromise
.then( response => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then( json => {
console.log(json[0].name);
});
错误捕获
这给我们带来了最后一个问题:我们如何处理错误?fetch()
API 可能因为很多原因抛出错误(例如,没有网络连接或 URL 本身存在问题),我们也会在服务器返回错误消息时抛出一个错误。
在上面文章中,我们看到在嵌套回调中进行错误处理非常困难,我们需要在每一个嵌套层中单独捕获错误。
Promise
对象提供了一个 catch()
方法来支持错误处理。这很像 then()
:你调用它并传入一个处理函数。然后,当异步操作成功时,传递给 then()
的处理函数被调用,而当异步操作失败时,传递给 catch()
的处理函数被调用。
如果将 catch()
添加到 Promise 链的末尾,它就可以在任何异步函数失败时被调用。于是,我们就可以将一个操作实现为几个连续的异步函数调用,并在一个地方处理所有错误。
试试这个版本的 fetch()
代码。我们使用 catch()
添加了一个错误处理函数,并修改了 URL(这样请求就会失败)。
const fetchPromise = fetch('bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
fetchPromise
.then( response => {
if (!response.ok) {
throw new Error(`HTTP 请求错误:${response.status}`);
}
return response.json();
})
.then( json => {
console.log(json[0].name);
})
.catch( error => {
console.error(`无法获取产品列表:${error}`);
});
Promise状态
Promise 中有一些具体的术语值得我们弄清楚。
首先,Promise 有三种状态:
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。这是调用
fetch()
返回 Promise 时的状态,此时请求还在进行中。 - 已兑现(fulfilled):意味着操作成功完成。当 Promise 完成时,它的
then()
处理函数被调用。 - 已拒绝(rejected):意味着操作失败。当一个 Promise 失败时,它的
catch()
处理函数被调用。
注意,这里的“成功”或“失败”的含义取决于所使用的 API:例如,
fetch()
认为服务器返回一个错误(如404 Not Found)时请求成功,但如果网络错误阻止请求被发送,则认为请求失败。
有时我们用 已敲定(settled) 这个词来同时表示 已兑现(fulfilled) 和 已拒绝(rejected) 两种情况。
如果一个 Promise 处于**已决议(resolved)**状态,或者它被“锁定”以跟随另一个 Promise 的状态,那么它就是 已兑现(fulfilled)。
文章 Let’s talk about how to talk about promises 对这些术语的细节做了很好的解释。
合并使用多个 Promise
当你的操作由几个异步函数组成,而且你需要在开始下一个函数之前完成之前每一个函数时,你需要的就是 Promise 链。但是在其他的一些情况下,你可能需要合并多个异步函数的调用,Promise
API 为解决这一问题提供了帮助。
有时你需要所有的 Promise 都得到实现,但它们并不相互依赖。在这种情况下,将它们一起启动然后在它们全部被兑现后得到通知会更有效率。这里需要 Promise.all()
方法。它接收一个 Promise 数组,并返回一个单一的 Promise。
由Promise.all()
返回的 Promise:
- 当且仅当数组中所有的 Promise 都被兑现时,才会通知
then()
处理函数并提供一个包含所有响应的数组,数组中响应的顺序与被传入all()
的 Promise 的顺序相同。 - 会被拒绝——如果数组中有任何一个 Promise 被拒绝。此时,
catch()
处理函数被调用,并提供被拒绝的 Promise 所抛出的错误。
譬如:
const fetchPromise1 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
const fetchPromise2 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found');
const fetchPromise3 = fetch('https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json');
Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
.then( responses => {
for (const response of responses) {
console.log(`${response.url}:${response.status}`);
}
})
.catch( error => {
console.error(`获取失败:${error}`)
});
这里我们向三个不同的 URL 发出三个 fetch()
请求。如果它们都被兑现了,我们将输出每个请求的响应状态。如果其中任何一个被拒绝了,我们将输出失败的情况。
根据我们提供的 URL,应该所有的请求都会被兑现,尽管因为第二个请求中请求的文件不存在,服务器将返回 404
(Not Found)而不是 200
(OK)。所以输出应该是:
如果我们用一个错误编码的 URL 尝试同样的代码,就像这样:
const fetchPromise1 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
const fetchPromise2 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found');
const fetchPromise3 = fetch('bad-scheme://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json');
Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
.then( responses => {
for (const response of responses) {
console.log(`${response.url}:${response.status}`);
}
})
.catch( error => {
console.error(`获取失败:${error}`)
});
……然后 catch()
处理程序将被运行,我们应该看到像这样的输出:
有时,你可能需要等待一组 Promise 中的某一个 Promise 的执行,而不关心是哪一个。在这种情况下,你需要 Promise.any()
。这就像 Promise.all()
,不过在 Promise 数组中的任何一个被兑现时它就会被兑现,如果所有的 Promise 都被拒绝,它也会被拒绝。
const fetchPromise1 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
const fetchPromise2 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found');
const fetchPromise3 = fetch('https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json');
Promise.any([fetchPromise1, fetchPromise2, fetchPromise3])
.then( response => {
console.log(`${response.url}:${response.status}`);
})
.catch( error => {
console.error(`获取失败:${error}`)
});
值得注意的是,在这种情况下,我们无法预测哪个获取请求会先被兑现。
这两个用于组合多个承诺的函数只是额外的 Promise
函数中的两个。要了解其余的内容,参见 Promise
参考文档。
async 和 await
async
关键字为你提供了一种更简单的方法来处理基于异步 Promise 的代码。在一个函数的开头添加 async
,就可以使其成为一个异步函数。
async function myFunction() {
// 这是一个异步函数
}
在异步函数中,你可以在调用一个返回 Promise 的函数之前使用 await
关键字。这使得代码在该点上等待,直到 Promise 被完成,这时 Promise 的响应被当作返回值,或者被拒绝的响应被作为错误抛出。
这使你能够编写像同步代码一样的异步函数。例如,我们可以用它来重写我们的 fetch 示例。
async function fetchProducts() {
try {
// 在这一行之后,我们的函数将等待 `fetch()` 调用完成
// 调用 `fetch()` 将返回一个“响应”或抛出一个错误
const response = await fetch(
'https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json'
);
if (!response.ok) {
throw new Error(`HTTP 请求错误:${response.status}`);
} else {
console.log(response);
}
// 在这一行之后,我们的函数将等待 `response.json()` 的调用完成
// `response.json()` 调用将返回 JSON 对象或抛出一个错误
const json = await response.json();
console.log(json[0].name);
} catch (error) {
console.error(`无法获取产品列表:${error}`);
}
}
这里我们调用 await fetch()
,我们的调用者得到的并不是 Promise
,而是一个完整的 Response
对象,就好像 fetch()
是一个同步函数一样。
我们甚至可以使用 try...catch
块来处理错误,就像我们在写同步代码时一样。
但请注意,这个写法只在异步函数中起作用。异步函数总是返回一个 Pomise,所以你不能做这样的事情:
async function fetchProducts() {
try {
const response = await fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
if (!response.ok) {
throw new Error(`HTTP 请求错误:${response.status}`);
}
const json = await response.json();
return json;
}
catch(error) {
console.error(`无法获取产品列表:${error}`);
}
}
const json = fetchProducts();
console.log(json[0].name); // json 是一个 Promise 对象,因此这句代码无法正常工作
相反,你需要做一些事情,比如:
async function fetchProducts() {
try {
const response = await fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
if (!response.ok) {
throw new Error(`HTTP 请求错误:${response.status}`);
}
const json = await response.json();
return json;
}
catch(error) {
console.error(`无法获取产品列表:${error}`);
}
}
const jsonPromise = fetchProducts();
jsonPromise.then((json) => console.log(json[0].name));
你可能会在需要使用 Promise 链地方使用 async
函数,这也使得 Promise 的工作更加直观。
请记住,就像一个 Promise 链一样,await
强制异步操作以串联的方式完成。如果下一个操作的结果取决于上一个操作的结果,这是必要的,但如果不是这样,像 Promise.all()
这样的操作会有更好的性能。
Promise使用
Promise 对象用于表示一个异步操作的最终完成(或失败)及其结果值。
通过new
关键字创建promise
对象
new Promise(function (resolve, reject) {
setTimeout(() => {
console.log('随便输出一句话');
resolve('resolve--->success');
}, 3000);
});
参数说明
resolve
:表示执行成功的回调reject
:表示执行失败的回调
resolve
是将Promise的状态置为fullfiled
, reject
是将Promise的状态置为rejected
。
上面的代码中,直接通过new
创建了Promise
对象,并没有调用它,但是传进去的函数已经执行了。因此我们用Promise的时候—般是包含在一个函数中,在需要的时候去运行这个函数。
function fun() {
let p = new Promise(function (resolve, reject) {
setTimeout(() => {
console.log('随便输出一句话');
resolve('resolve--->success');
}, 3000);
});
return p; // 返回Promise对象
}
在我们包装好的函数最后,通过return
返回了一个Promise
对象。也就是说调用fun()
函数可以得到Promise
的对象,接下来就可以使用Promise对象上的then
与catch
方法了。
let my_promise = fun(); // 调用函数,可以得到Promise对象,然后可以调用它的then和catch方法
my_promise.then(function (data) {
console.log('promise.then里的data--->', data); // 这个data是resolve里的数据,后面可以用传过来的数据做些其它的操作...
});
在fun()
的返回上直接调用then
方法 ,then
接收一个参数,是函数,函数的参数data
将会拿到我们在fun()
中调用resolve
时传的的参数。
此时then
里面的函数就是回调函数,能够在fun()
这个异步任务执行完成之后被执行。这就是Promise
的作用了,简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。
promise的链式调用
从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用。
现在再来看一下promise的链式调用。
function fn1() {
let p = new Promise(function (resolve, reject) {
// 做一些异步操作
setTimeout(function () {
console.log('异步任务1执行完成');
resolve('one');
}, 2000);
});
return p;
}
function fn2() {
return new Promise((resolve, reject) => {
// 做一些异步操作
setTimeout(() => {
console.log('异步任务2执行完成');
resolve('two');
}, 1000);
});
}
function fn3() {
let p = new Promise(function (resolve, reject) {
// 做一些异步操作
setTimeout(function () {
console.log('异步任务3执行完成');
resolve('three');
}, 3000);
});
return p;
}
链式调用:
fn1()
.then(function (data) {
console.log(data);
return fn2();
})
.then((res) => {
console.log(res);
return fn3();
})
.then((result) => {
console.log(result);
});
结果:
reject的用法
reject
的作用就是把Promise
的状态置为rejected
,这样我们在then
中就能捕捉到,然后执行“失败”情况的回调。
function fn() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// Math.random 生成0~1的随机数
// Math.ceil 向上取整
let num = Math.ceil(Math.random() * 10); // 生成1~10的随机整数
if (num <= 5) {
resolve(`执行成功,num的值为:${num}`);
} else {
reject(`执行失败,该数字大于5,num的值为${num}`);
}
}, 1000);
});
}
then
方法可以接受两个参数,第一个对应resolve
的回调,第二个对应reject
的回调。
fn().then(
function (data) {
console.log('resolve回调', data);
},
function (error) {
console.log('reject回调', error);
}
);
而当只传入一个参数时,控制台会抛出错误
fn().then(function (data) {
console.log('resolve回调', data);
});
catch的用法
而这时如果用catch
捕获异常得到的结果和在then
方法中传入第二个参数的效果是一样的。
fn()
.then(function (data) {
console.log('resolve回调', data);
})
.catch(function (error) {
console.log('catch error', error);
});
效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve
的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。
function fetchApi() {
fn()
.then(function (data) {
console.log('resolve回调', data);
console.log(somedata); // somedata未定义
})
.catch(function (error) {
console.log('catch error', error);
});
}
在resolve
的回调中,我们使用了console.log(somedata);
而somedata
这个变量是没有被定义的。如果我们不用Promise
,代码运行到这里就直接在控制台报错了,不往下运行了。但是在这里,会得到这样的结果:
也就是说进到catch
方法里面,即便是有错误的代码也不会报错了,这与我们的try...catch
语句有相同的功能。