异步 是我们在阅读技能文章时经常看到的字眼,那 异步 是什么意思?他紧张吗?要怎么实现异步呢?本文将试着分析清楚这些事变。
异步 JavaScript 简介
异步编程技能使你的步伐可以在实行一个大概长期运行的任务的同时继承对其他变乱做出反应而不必等候任务完成。与此同时,你的步伐也将在任务完成后表现结果。
欣赏器提供的很多功能(尤其是最风趣的那一部分)大概需要很长的时间来完成,因此需要异步完成,比方:
- 利用 fetch() 发起 HTTP 哀求
- 利用 getUserMedia() 访问用户的摄像头和麦克风
- 利用 showOpenFilePicker() 哀求用户选择文件以供访问
因此,即使你大概不需要经常实现自己的异步函数,你也很大概需要精确利用它们。
在这部分中,我们将从同步函数长时间运行时存在的题目开始,并以此进一步熟悉异步编程的须要性。
同步编程
观察下面的代码:
const name = 'Miriam';const greeting = `Hello, my name is ${name}!`;console.log(greeting);// "Hello, my name is Miriam!"这段代码:
- 声明确一个叫做 name 的字符串常量
- 声明确另一个叫做 greeting 的字符串常量(并利用了 name 常量的值)
- 将 greeting 常量输出到 JavaScript 控制台中。
我们应该注意的是,现实上欣赏器是按照我们誊写代码的次序一行一行地实行步伐的。欣赏器会等候代码的解析和工作,在上一行完成后才会实行下一行。如许做是很有须要的,由于每一行新的代码都是创建在前面代码的基础之上的。
这也使得它成为一个同步步伐。
究竟上,调用函数的时间也是同步的,就像如许:
function makeGreeting(name) { return `Hello, my name is ${name}!`;}const name = 'Miriam';const greeting = makeGreeting(name);console.log(greeting);// "Hello, my name is Miriam!"在这里 makeGreeting() 就是一个同步函数,由于在函数返回之前,调用者必须等候函数完成其工作。
一个耗时的同步函数
假如同步函数需要很长的时间怎么办?
当用户点击“天生素数”按钮时,这个步伐将利用一种非常低效的算法天生一些大素数。你可以控制要天生的素数数量,这也会影响操作需要的时间。
<label for="quota">素数个数:</label><input type="text" id="quota" name="quota" value="1000000"><button id="generate">天生素数</button><button id="reload">重载</button><div id="output"></div>function generatePrimes(quota) { function isPrime(n) { for (let c = 2; c <= Math.sqrt(n); ++c) { if (n % c === 0) { return false; } } return true; } const primes = []; const maximum = 1000000; while (primes.length < quota) { const candidate = Math.floor(Math.random() * (maximum + 1)); if (isPrime(candidate)) { primes.push(candidate); } } return primes;}document.querySelector('#generate').addEventListener('click', () => { const quota = document.querySelector('#quota').value; const primes = generatePrimes(quota); document.querySelector('#output').textContent = `完成!已天生素数${quota}个。`;});document.querySelector('#reload').addEventListener('click', () => { document.location.reload()});耗时同步函数的题目
接下来的示例和上一个一样,不外我们增长了一个文本框供你输入。这一次,试着点击“天生素数”,然后在文本框中输入。
你会发现,当我们的 generatePrimes() 函数运行时,我们的步伐完全没有反应:用户不能输入任何东西,也不能点击任何东西,或做任何其他事变。
这就是耗时的同步函数的根本题目。在这里我们想要的是一种方法,以让我们的步伐可以:
- 通过调用一个函数来启动一个长期运行的操作
- 让函数开始操作并立刻返回,如许我们的步伐就可以保持对其他变乱做出反应的本事
- 当操作终极完成时,关照我们操作的结果。
这就是异步函数为我们提供的本事,本模块的别的部分将表明它们是如安在 JavaScript 中实现的。
变乱处理处罚步伐
我们刚才看到的对异步函数的形貌大概会让你想起变乱处理处罚步伐,这么想是对的。变乱处理处罚步伐现实上就是异步编程的一种形式:你提供的函数(变乱处理处罚步伐)将在变乱发生时被调用(而不是立刻被调用)。假如“变乱”是“异步操作已经完成”,那么你就可以看到变乱怎样被用来关照调用者异步函数调用的结果的。
一些早期的异步 API 正是以这种方式来利用变乱的。XMLHttpRequest API 可以让你用 JavaScript 向长途服务器发起 HTTP 哀求。由于如许的操作大概需要很长的时间,以是它被计划成异步 API,你可以通过给 XMLHttpRequest 对象附加变乱监听器来让步伐在哀求盼望和终极完成时得到关照。
下面的例子展示了如许的操作。点击“点击发起哀求”按钮来发送一个哀求。我们将创建一个新的 XMLHttpRequest 并监听它的 loadend 变乱。而我们的变乱处理处罚步伐则会在控制台中输出一个“完成!”的消息和哀求的状态代码。
我们在添加了变乱监听器后发送哀求。注意,在这之后,我们仍旧可以在控制台中输出“哀求已发起”,也就是说,我们的步伐可以在哀求举行的同时继承运行,而我们的变乱处理处罚步伐将在哀求完成时被调用。
<button id="xhr">点击发起哀求</button><button id="reload">重载</button><pre readonlyhttps://mdn.github.io/learning-area/javascript/apis/can-store/products.json下载 JSON 文件,并记录一些相干信息。</p>在这篇文章中,我们将通过复制页面上的代码示例到欣赏器的 JavaScript 控制台中运行的方式来学习 Promise。因此在正式开始学习之前你需要举行以下设置:
- 在欣赏器的新标签页中访问 https://example.org。
- 在该标签页中,打开 欣赏器开辟者工具 中的 JavaScript 控制台
- 把我们展示的代码示例复制到控制台中运行。值得注意的是,你必须在每次输入新的示例之前重新加载页面,否则控制台会报错“重新界说了 fetchPromise”。
要做到这一点,我们将向服务器发出一个 HTTP 哀求。在 HTTP 哀求中,我们向长途服务器发送一个哀求信息,然后它向我们发送一个相应。这次,我们将发送一个哀求,从服务器上得到一个 JSON 文件。还记得在上一篇文章中,我们利用 XMLHttpRequest API 举行 HTTP 哀求吗?那么,在这篇文章中,我们将利用 fetch() API,一个当代的、基于 Promise 的、用于替换 XMLHttpRequest 的方法。
把下列代码复制到你的欣赏器 JavaScript 控制台中:
const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/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 对象。
- 输出一条信息,分析我们已经发送了这个哀求。
完备的输出结果应该是如许的:
已发送哀求……已收到相应:200请注意,已发送哀求…… 的消息在我们收到相应之前就被输出了。与同步函数差异,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/can-store/products.json');fetchPromise.then( response => { const jsonPromise = response.json(); jsonPromise.then( json => { console.log(json[0].name); });});在这个示例中,就像我们之前做的那样,我们给 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/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/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//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}`); });尝试运行这个版本:你应该会看到 catch() 处理处罚函数输出的错误。
Promise 术语
Promise 中有一些详细的术语值得我们弄清楚。
起首,Promise 有三种状态:
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。这是调用 fetch() 返回 Promise 时的状态,此时哀求还在举行中。
- 已兑现(fulfilled):意味着操作乐成完成。当 Promise 完成时,它的 then() 处理处罚函数被调用。
- 已拒绝(rejected):意味着操作失败。当一个 Promise 失败时,它的 catch() 处理处罚函数被调用。
注意,这里的“乐成”或“失败”的寄义取决于所利用的 API:比方,fetch() 以为服务器返回一个错误(如 404 Not Found )时哀求乐成,但假如网络错误克制哀求被发送,则以为哀求失败。
偶然我们用 已敲定(settled) 这个词来同时表现 已兑现(fulfilled) 和 已拒绝(rejected) 两种环境。
假如一个 Promise 处于已决定(resolved)状态,或者它被“锁定”以跟随另一个 Promise 的状态,那么它就是 已兑现(fulfilled)。
归并利用多个 Promise
当你的操作由几个异步函数构成,而且你需要在开始下一个函数之前完成之前每一个函数时,你需要的就是 Promise 链。但是在其他的一些环境下,你大概需要归并多个异步函数的调用,Promise API 为办理这一题目提供了资助。
偶然你需要全部的 Promise 都得到实现,但它们并不相互依赖。在这种环境下,将它们一起启动然后在它们全部被兑现后得到关照会更有服从。这里需要 Promise.all() 方法。它吸取一个 Promise 数组,并返回一个单一的 Promise。 |