自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Async/Await初學者指南

開發(fā) 前端
JavaScript中的async和await關鍵字提供了一種現(xiàn)代語法,幫助我們處理異步操作。在本教程中,我們將深入研究如何使用async/await來掌控JavaScript程序中的流程控制。

在JavaScript中,一些操作是異步的。這意味著它們產(chǎn)生的結(jié)果或者值不會立即奏效。

看看下面的代碼:

function fetchDataFromApi() {
  // Data fetching logic here
  console.log(data);
}

fetchDataFromApi();
console.log('Finished fetching data');

JavaScript解釋器不會等待異步fetchDataFromApi函數(shù)完成后再解釋下一條語句。因此,在打印API返回的真實數(shù)據(jù)之前,它就會打印Finished fetching data。

大多數(shù)情況下,這并不是我們想要的行為。幸運的是,我們可以使用async和await關鍵字,使我們的程序在繼續(xù)前進之前等待異步操作的完成。

這個功能是在ES2017引入JavaScript的,在所有現(xiàn)代瀏覽器[1]中都支持。

如何創(chuàng)建JavaScript異步函數(shù)

讓我們近距離看看fetchDataFromApi數(shù)據(jù)獲取的邏輯。在JavaScript中,數(shù)據(jù)獲取是典型的異步操作案例。

使用Fetch API,我們可以這么做:

function fetchDataFromApi() {
  fetch('https://v2.jokeapi.dev/joke/Programming?type=single')
    .then(res => res.json())
    .then(json => console.log(json.joke));
}

fetchDataFromApi();
console.log('Finished fetching data');

這里,我們從JokeAPI[2]獲取一個編程笑話。API的響應是JSON格式的,所以我們在請求完成后提取該響應(使用json()方法),然后把這個笑話打印到控制臺。

請注意,JokeAPI是第三方API,我們不能保證返回笑話的質(zhì)量。

如果在瀏覽器中運行該代碼,或者在Node中(17.5+版本中使用--experimental-fetch)運行,我們將看到,事情仍然以錯誤的順序打印在控制臺中。

讓我們來改變它。

async關鍵字

我們需要做的第一件事是將包含的函數(shù)標記為異步的。我們可以通過使用async關鍵字來做到這一點,我們把它放在function關鍵字的前面:

async function fetchDataFromApi() {
  fetch('https://v2.jokeapi.dev/joke/Programming?type=single')
    .then(res => res.json())
    .then(json => console.log(json.joke));
}

異步函數(shù)總是返回一個promise(后面會詳細介紹),所以可以通過在函數(shù)調(diào)用上鏈接一個then()來獲得正確的執(zhí)行順序:

fetchDataFromApi()
  .then(() => {
    console.log('Finished fetching data');
  });

如果現(xiàn)在運行代碼,看到的結(jié)果會是這樣的:

If Bill Gates had a dime for every time Windows crashed ... Oh wait, he does.
Finished fetching data

但我們并不想這樣做!JavaScript的promise語法可能會有點毛糙,而這正是async/await的優(yōu)勢所在:它使我們能夠用一種看起來更像同步代碼的語法來編寫異步代碼,而且更容易閱讀。

await關鍵字

接下來要做的是,在我們的函數(shù)中的任何異步操作前面加上 await 關鍵字。這將迫使JavaScript解釋器"暫停"執(zhí)行并等待結(jié)果。我們可以將這些操作的結(jié)果分配給變量:

async function fetchDataFromApi() {
  const res = await fetch('https://v2.jokeapi.dev/joke/Programming?type=single');
  const json = await res.json();
  console.log(json.joke);
}

我們還需要等待調(diào)用fetchDataFromApi函數(shù)的結(jié)果:

await fetchDataFromApi();
console.log('Finished fetching data');

很不幸,如果嘗試運行代碼,會得到一個錯誤:

Uncaught SyntaxError: await is only valid in async functions, async generators and modules

這是因為我們不能在非模塊腳本中的async函數(shù)之外使用await。我們將在后面詳細討論這個問題,但現(xiàn)在解決這個問題的最簡單的方法是將調(diào)用的代碼包裹在一個自己的函數(shù)中,我們也會將其標記為async:

async function fetchDataFromApi() {
  const res = await fetch('https://v2.jokeapi.dev/joke/Programming?type=single');
  const json = await res.json();
  console.log(json.joke);
}

async function init() {
  await fetchDataFromApi();
  console.log('Finished fetching data');
}

init();

如果現(xiàn)在運行代碼,一切都如愿:

UDP is better in the COVID era since it avoids unnecessary handshakes.
Finished fetching data

我們需要這個額外的模板是不幸的,但在我看來,這個代碼仍然比基于promise的版本更容易閱讀。

聲明異步函數(shù)的不同方式

先前的例子中,使用了兩個具名函數(shù)聲明(function關鍵字后跟著函數(shù)名字),但我們并不局限于這些。我們也可以把函數(shù)表達式、箭頭函數(shù)和匿名函數(shù)標記為async。

「異步函數(shù)表達式」

當我們創(chuàng)建一個函數(shù),并將其賦值給一個變量時,這便是「函數(shù)表達式」。該函數(shù)是匿名的,這意味著它沒有名字。比如:

const fetchDataFromApi = async function() {
  const res = await fetch('https://v2.jokeapi.dev/joke/Programming?type=single');
  const json = await res.json();
  console.log(json.joke);
}

這將以與我們之前的代碼完全相同的方式工作。

「異步箭頭函數(shù)」

箭頭函數(shù)在ES6被引入。它們是函數(shù)表達式的緊湊替代品,并且總是匿名的。它們的基本語法如下:

(params) => { <function body> }

為了標記箭頭函數(shù)為匿名的,在左括號前插入async關鍵字。

舉個例子,除了在上面的代碼中創(chuàng)建一個額外的init函數(shù)外,另一個辦法是將現(xiàn)有的代碼包裹在一個IIFE中,我們將其標記為async:

(async () => {
  async function fetchDataFromApi() {
    const res = await fetch('https://v2.jokeapi.dev/joke/Programming?type=single');
    const json = await res.json();
    console.log(json.joke);
  }
  await fetchDataFromApi();
  console.log('Finished fetching data');
})();

使用函數(shù)表達式或函數(shù)聲明并沒有什么大的區(qū)別:大部分情況下,這只是一個使用偏好的問題。但有幾件事情需要注意,比如變量提升,或者箭頭函數(shù)無法綁定this的事實。

Await/Async內(nèi)部機制

正如你可能已經(jīng)猜到的,async/await在很大程度上是promise的語法糖。讓我們更詳細地看一下這個問題,因為更好地理解內(nèi)部發(fā)生的事情將對理解async/await的工作方式有很大幫助。

第一件需要注意的事情是,async函數(shù)總是返回一個promise,即使我們不顯式地告訴它這么做。比如:

async function echo(arg) {
  return arg;
}

const res = echo(5);
console.log(res);

打印結(jié)果如下:

Promise { <state>: "fulfilled", <value>: 5 }

promise可能會是三種狀態(tài)之一:pending、fulfilled、或者rejected。一個promise開始時處于pending狀態(tài)。如果與該promise有關的行為成功了,該promise就被稱為fulfilled。如果行為不成功,該promise就被稱為rejected。一旦promise是fulfilled或者rejected,但不是pending,它也被認為是settled。

當我們在async函數(shù)中使用 await 關鍵字來"暫停"函數(shù)執(zhí)行時,真正發(fā)生的是我們在等待一個promise(無論是顯式還是隱式)進入resolved或rejected狀態(tài)。

基于上述示例,我們可以這么做:

async function echo(arg) {
  return arg;
}

async function getValue() {
  const res = await echo(5);
  console.log(res);
}

getValue();
// 5

因為echo函數(shù)返回一個promise,而getValue函數(shù)中的await關鍵字在繼續(xù)程序之前等待這個promise完成,所以我們能夠?qū)⑺璧闹荡蛴〉娇刂婆_。

promise是對JavaScript中流程控制的一大改進,并且被一些較新的瀏覽器API所使用。比如Battery status API[3]、Clipboard API[4]、Fetch API[5]、MediaDevices API[6]等等。

Node還在其內(nèi)置的util模塊中添加了一個promise函數(shù),可以將使用回調(diào)函數(shù)的代碼轉(zhuǎn)換為返回promise。而從v10開始,Node的fs模塊中的函數(shù)可以直接返回promise。

從promise到async/await的轉(zhuǎn)換

那么,為什么這一切對我們來說都很重要呢?

好消息是,任何返回promise的函數(shù)都可以使用async/await。我并不是說我們應該對所有的事情都使用async/await(該語法確實有其缺點,我們將在討論錯誤處理時看到),但我們應該意識到這是可能的。

我們已經(jīng)看到了如何改變基于promise的獲取調(diào)用,使之與async/await一起工作,所以讓我們看另一個例子。這里有一個小的實用函數(shù),使用Node基于promise的API和它的readFile方法來獲取一個文件的內(nèi)容。

使用Promise.then():

const { promises: fs } = require('fs');

const getFileContents = function(fileName) {
  return fs.readFile(fileName, enc)
}

getFileContents('myFile.md', 'utf-8')
  .then((contents) => {
    console.log(contents);
  });

有了async/await就會變成:

import { readFile } from 'node:fs/promises';

const getFileContents = function(fileName, enc) {
  return readFile(fileName, enc)
}

const contents = await getFileContents('myFile.md', 'utf-8');
console.log(contents);

注意:這是在利用一個叫做top-level await的功能,它只在ES模塊中可用。要運行這段代碼,請將文件保存為index.mjs并使用Node>=14.8的版本。

雖然這些都是簡單的例子,但我發(fā)現(xiàn)async/await的語法更容易理解。當處理多個then()語句和錯誤處理時,這一點變得尤其真實。

錯誤處理

在處理異步函數(shù)時,有幾種方法來處理錯誤。最常見的可能是使用try...catch塊,我們可以把它包在異步操作中并捕捉任何發(fā)生的錯誤。

在下面的例子中,請注意我是如何將URL改成不存在的東西的:

async function fetchDataFromApi() {
  try {
    const res = await fetch('https://non-existent-url.dev');
    const json = await res.json();
    console.log(json.joke);
  } catch (error) {
    // Handle the error here in whichever way you like
    console.log('Something went wrong!');
    console.warn(error)
  }
}

await fetchDataFromApi();
console.log('Finished fetching data');

這將導致以下信息被打印到控制臺:

Something went wrong!
TypeError: fetch failed
    ...
    cause: Error: getaddrinfo ENOTFOUND non-existent-url.dev
Finished fetching data

這種結(jié)果是因為fetch返回一個promise。當fetch操作失敗時,promise的reject方法被調(diào)用,await關鍵字將這種reject轉(zhuǎn)換為一個可捕捉的錯誤。

然而,這種方法有幾個問題。主要的問題是它很啰嗦,而且相當難看。想象一下,我們正在構(gòu)建一個CRUD應用程序,我們?yōu)槊總€CRUD方法(創(chuàng)建、讀取、更新、銷毀)都有一個單獨的函數(shù)。如果這些方法中的每一個都進行了異步API調(diào)用,我們就必須把每個調(diào)用包在自己的try...catch塊中。這是相當多的額外代碼。

另一個問題是,如果我們不使用await關鍵字,這將導致一個未處理的拒絕的promise:

import { readFile } from 'node:fs/promises';

const getFileContents = function(fileName, enc) {
  try {
    return readFile(fileName, enc)
  } catch (error) {
    console.log('Something went wrong!');
    console.warn(error)
  }
}

const contents = await getFileContents('this-file-does-not-exist.md', 'utf-8');
console.log(contents);

上述代碼的打印如下:

node:internal/process/esm_loader:91
    internalBinding('errors').triggerUncaughtException(
                              ^
[Error: ENOENT: no such file or directory, open 'this-file-does-not-exist.md'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'this-file-does-not-exist.md'
}

與await不同,return關鍵字不會將拒絕的promise轉(zhuǎn)化為可捕捉的錯誤。

在函數(shù)調(diào)用中使用catch()

每個返回promise的函數(shù)都可以利用promise的catch方法來處理任何可能發(fā)生的promise拒絕。

有了這個簡單的補充,上例中的代碼將優(yōu)雅地處理錯誤:

const contents = await getFileContents('this-file-does-not-exist.md', 'utf-8')
  .catch((error) => {
    console.log('Something went wrong!');
    console.warn(error);
  });
console.log(contents);

現(xiàn)在輸出是這樣子的:

Something went wrong!
[Error: ENOENT: no such file or directory, open 'this-file-does-not-exist.md'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'this-file-does-not-exist.md'
}
undefined

至于使用哪種策略,我同意Valeri Karpov[7]的建議。使用try/catch來恢復async函數(shù)內(nèi)部的預期錯誤,但通過在調(diào)用函數(shù)中添加catch()來處理意外錯誤。

并行運行異步命令

當我們使用await關鍵字來等待一個異步操作完成時,JavaScript解釋器會相應地暫停執(zhí)行。雖然這很方便,但這可能并不總是我們想要的??紤]一下下面的代碼:

(async () => {
  async function getStarCount(repo){
    const repoData = await fetch(repo);
    const repoJson = await repoData.json()
    return repoJson.stargazers_count;
  }

  const reactStars = await getStarCount('https://api.github.com/repos/facebook/react');
  const vueStars = await getStarCount('https://api.github.com/repos/vuejs/core');
  console.log(`React has ${reactStars} stars, whereas Vue has ${vueStars} stars`)
})();

這里我們正在進行兩次API調(diào)用,分別獲取React和Vue的GitHub star數(shù)。雖然這樣可以正常運轉(zhuǎn),但我們沒有理由在發(fā)出第二個fetch請求之前等待第一個promise完成。如果我們要發(fā)出很多請求,這將是一個相當大的瓶頸。

為了解決這個問題,我們可以使用Promise.all,它接收一個promise數(shù)組,并等待所有promise被解決或其中任何一個承諾被拒絕:

(async () => {
  async function getStarCount(repo){
    // As before
  }

  const reactPromise = getStarCount('https://api.github.com/repos/facebook/react');
  const vuePromise = getStarCount('https://api.github.com/repos/vuejs/core');
  const [reactStars, vueStars] = await Promise.all([reactPromise, vuePromise]);

  console.log(`React has ${reactStars} stars, whereas Vue has ${vueStars} stars`);
})();

好多了!

同步循環(huán)中的異步await

在某些時候,我們會嘗試在一個同步循環(huán)中調(diào)用一個異步函數(shù)。比如說:

// Return promise which resolves after specified no. of milliseconds
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

async function process(array) {
  array.forEach(async (el) => {
    await sleep(el); // we cannot await promise here
    console.log(el);
  });
}

const arr = [3000, 1000, 2000];
process(arr);

這不會像預期的那樣奏效,因為forEach只會調(diào)用函數(shù)而不等待它完成,以下內(nèi)容將被打印到控制臺:

1000
2000
3000

同樣的事情也適用于其他許多數(shù)組方法,如map、filter和reduce。

幸運的是,ES2018引入了異步迭代器,除了它們的next()方法會返回一個promise外,它們就像普通的迭代器。這意味著我們可以在其中使用 await。讓我們使用for...of重寫上面的代碼:

async function process(array) {
  for (el of array) {
    await sleep(el);
    console.log(el);
  };
}

現(xiàn)在,process函數(shù)的輸出就是正確的順序:

3000
1000
2000

就像我們之前等待異步fetch請求的例子一樣,這也會帶來性能上的代價。for循環(huán)中的每個await都會阻塞事件循環(huán),通常應該重構(gòu)代碼,一次性創(chuàng)建所有的promise,然后使用Promise.all()來獲取結(jié)果。

甚至有一條ESLint規(guī)則[8],如果它檢測到這種行為就會警告。

頂層await

最后,讓我們來看看一個叫做「頂層await」的東西。這是ES2022中引入的語言,從14.8版開始在Node中可用。

當我們在文章開頭運行我們的代碼時,我們已經(jīng)被這個東西所要解決的問題給纏住了。還記得這個錯誤嗎?

Uncaught SyntaxError: await is only valid in async functions, async generators and modules

當我們試圖在一個async函數(shù)之外使用await時,就會發(fā)生這種情況。例如,在我們代碼的頂層:

const ms = await Promise.resolve('Hello, World!');
console.log(msg);

頂層await解決了這個問題,使上述代碼有效,但只在ES模塊中奏效。如果我們在瀏覽器中工作,我們可以把這段代碼添加到一個叫做index.js的文件中,然后像這樣把它加載到我們的頁面中:

<script src="index.js" type="module"></script>

事情會像預期的那樣工作,不需要包裝函數(shù)或丑陋的IIFE。

在Node中,事情變得更加有趣。要將一個文件聲明為ES模塊,我們應該做兩件事中的一件。一種方法是以.mjs為擴展名保存,然后像這樣運行它:

node index.mjs

另一種方法是在package.json文件中設置"type": "module":

{
  "name": "myapp",
  "type": "module",
  ...
}

頂層 await 也可以和動態(tài)導入很好地配合--一種類函數(shù)的表達式,它允許我們異步加載 ES 模塊。這將返回一個promise,而這個promise將被解析為一個模塊對象,這意味著我們可以這樣做:

const locale = 'DE';

const { default: greet } = await import(
  `${ locale === 'DE' ?
      './de.js' :
      './en.js'
  }`
);

greet();
// Outputs "Hello" or "Guten Tag" depending on the value of the locale variable

動態(tài)導入選項也很適合與React和Vue等框架相結(jié)合的懶加載。這使我們能夠減少初始包的大小和交互指標的時間。

總結(jié)

在這篇文章中,我們研究了如何使用async/await來管理你的JavaScript程序的控制流。我們討論了語法、async/await如何工作、錯誤處理,以及一些問題。如果你已經(jīng)走到了這一步,你現(xiàn)在就是一個專家了。 ??

編寫異步代碼可能很難,特別是對初學者來說,但現(xiàn)在你已經(jīng)對這些技術有了扎實的了解,你應該能夠運用它們來獲得巨大的效果。

  • 本文譯自:https://www.sitepoint.com/javascript-async-await/

以上就是本文的全部內(nèi)容,如果對你有所幫助,歡迎點贊、收藏、轉(zhuǎn)發(fā)~

參考資料

[1]現(xiàn)代瀏覽器:https://caniuse.com/async-functions

[2]JokeAPI:https://jokeapi.dev/

[3]Battery status API:https://developer.mozilla.org/en-US/docs/Web/API/Battery_Status_API

[4]Clipboard API:https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API

[5]Fetch API:https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API

[6]MediaDevices API:https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices

[7]Valeri Karpov:https://thecodebarbarian.com/async-await-error-handling-in-javascript.html

[8]ESLint規(guī)則:https://eslint.org/docs/latest/rules/no-await-in-loop

責任編輯:武曉燕 來源: 前端F2E
相關推薦

2022-04-24 15:21:01

MarkdownHTML

2021-05-10 08:50:32

網(wǎng)絡管理網(wǎng)絡網(wǎng)絡性能

2022-03-28 09:52:42

JavaScript語言

2023-07-03 15:05:07

預測分析大數(shù)據(jù)

2010-06-13 11:13:38

UML初學者指南

2022-07-22 13:14:57

TypeScript指南

2012-03-14 10:56:23

web app

2022-10-10 15:28:45

負載均衡

2023-02-10 08:37:28

2022-09-05 15:36:39

Linux日志記錄syslogd

2013-03-06 10:40:58

Adobe Edge HTML5

2010-08-26 15:47:09

vsftpd安裝

2013-04-08 16:35:52

Adobe Edge

2018-10-28 16:14:55

Reactreact.js前端

2011-03-02 10:57:27

vsFTPd

2023-02-19 15:31:09

架構(gòu)軟件開發(fā)代碼

2014-04-01 10:20:00

開源Rails

2020-08-16 13:10:46

TensorFlow深度學習數(shù)據(jù)集

2024-04-28 10:56:34

Next.jsWeb應用搜索引擎優(yōu)化

2021-05-06 09:00:00

JavaScript靜態(tài)代碼開發(fā)
點贊
收藏

51CTO技術棧公眾號