開発者ドキュメント

JavaScriptでのイベントループ、コールバック、プロミス、非同期/待機について理解する

序章

インターネットの初期には、WebサイトはHTMLページの静的データで構成されていました。 しかし、Webアプリケーションがよりインタラクティブで動的になった今、APIデータを取得するための外部ネットワーク要求を行うなどの集中的な操作を行うことがますます必要になっています。 JavaScriptでこれらの操作を処理するには、開発者は非同期プログラミング手法を使用する必要があります。

JavaScriptはシングルスレッドプログラミング言語であり、同期実行モデルが次々に操作を処理するため、一度に処理できるのは1つのステートメントのみです。 ただし、APIからのデータの要求などのアクションは、要求されるデータのサイズ、ネットワーク接続の速度、およびその他の要因によっては、不確定な時間がかかる場合があります。 API呼び出しが同期的に実行された場合、ブラウザは、その操作が完了するまで、スクロールやボタンのクリックなどのユーザー入力を処理できません。 これは、ブロッキングとして知られています。

ブロッキング動作を防ぐために、ブラウザ環境には、JavaScriptがアクセスできる非同期の多くのWeb APIがあります。つまり、それらは順番にではなく、他の操作と並行して実行できます。 これは、非同期操作の処理中にユーザーがブラウザーを通常どおり使用し続けることができるため便利です。

JavaScript開発者は、非同期Web APIを操作し、それらの操作の応答またはエラーを処理する方法を知っている必要があります。 この記事では、イベントループ、コールバックを介して非同期動作を処理する元の方法、更新された ECMAScript 2015 のpromiseの追加、および使用の最新の方法について学習します。 async/await.

注:この記事は、ブラウザー環境でのクライアント側のJavaScriptに焦点を当てています。 同じ概念がNode.js環境でも一般的に当てはまりますが、Node.jsはブラウザーの WebAPIではなく独自のC++APIを使用します。 Node.jsでの非同期プログラミングの詳細については、Node.jsで非同期コードを作成する方法を確認してください。

イベントループ

このセクションでは、JavaScriptがイベントループを使用して非同期コードを処理する方法について説明します。 まず、動作中のイベントループのデモンストレーションを実行し、次にイベントループの2つの要素であるスタックとキューについて説明します。

非同期WebAPIを使用しないJavaScriptコードは、同期的に1つずつ順番に実行されます。 これは、それぞれがconsoleに番号を出力する3つの関数を呼び出すこのサンプルコードによって示されます。

// Define three example functions
function first() {
  console.log(1)
}

function second() {
  console.log(2)
}

function third() {
  console.log(3)
}

このコードでは、数値を出力する3つの関数を定義します。 console.log().

次に、関数への呼び出しを記述します。

// Execute the functions
first()
second()
third()

出力は、関数が呼び出された順序に基づきます—first(), second()、 それから third():

Output
1 2 3

非同期WebAPIを使用すると、ルールがより複雑になります。 これをテストできる組み込みAPIは setTimeout、タイマーを設定し、指定された時間が経過するとアクションを実行します。 setTimeout 非同期である必要があります。そうしないと、待機中にブラウザ全体がフリーズしたままになり、ユーザーエクスペリエンスが低下します。

追加 setTimeoutsecond 非同期リクエストをシミュレートする関数:

// Define three example functions, but one of them contains asynchronous code
function first() {
  console.log(1)
}

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

function third() {
  console.log(3)
}

setTimeout 2つの引数を取ります。非同期で実行される関数と、その関数を呼び出す前に待機する時間です。 このコードでは、ラップしました console.log 匿名関数でそれをに渡しました setTimeout、次に実行する関数を設定します 0 ミリ秒。

前に行ったように、関数を呼び出します。

// Execute the functions
first()
second()
third()

あなたは setTimeout に設定 0 これらの3つの関数を実行すると、番号が順番に出力されます。 ただし、非同期であるため、タイムアウトのある関数は最後に出力されます。

Output
1 3 2

タイムアウトを0秒に設定しても5分に設定しても、違いはありません。 console.log 非同期コードによって呼び出されると、同期トップレベル関数の後に実行されます。 これは、JavaScriptホスト環境(この場合はブラウザー)がイベントループと呼ばれる概念を使用して、同時実行または並列イベントを処理するために発生します。 JavaScriptは一度に1つのステートメントしか実行できないため、どの特定のステートメントをいつ実行するかをイベントループに通知する必要があります。 イベントループは、スタックキューの概念でこれを処理します。

スタック

スタック、または呼び出しスタックは、現在実行されている関数の状態を保持します。 スタックの概念に慣れていない場合は、「後入れ先出し」(LIFO)プロパティを持つ配列として想像できます。つまり、最後からアイテムを追加または削除することしかできません。スタック。 JavaScriptは、スタック内の現在の frame (または特定の環境での関数呼び出し)を実行し、それを削除して次のフレームに移動します。

同期コードのみを含む例の場合、ブラウザは次の順序で実行を処理します。

2番目の例 setTimout このように見えます:

使用する setTimeout非同期WebAPIである、 queue の概念を紹介します。これについては、このチュートリアルで次に説明します。

キューは、メッセージキューまたはタスクキューとも呼ばれ、機能の待機領域です。 コールスタックが空の場合は常に、イベントループは、最も古いメッセージから開始して、待機中のメッセージがないかキューをチェックします。 見つかったら、それをスタックに追加し、メッセージ内の関数を実行します。

の中に setTimeout たとえば、匿名関数は、タイマーがに設定されているため、トップレベルの残りの実行の直後に実行されます 0 秒。 タイマーは、コードが正確に実行されることを意味するものではないことを覚えておくことが重要です。 0 秒または指定された時間は何でもですが、その時間内に匿名関数がキューに追加されます。 このキューシステムが存在するのは、タイマーの終了時にタイマーが匿名関数をスタックに直接追加すると、現在実行中の関数が中断され、意図しない予期しない影響が生じる可能性があるためです。

注:promiseを処理するジョブキューまたはマイクロタスクキューと呼ばれる別のキューもあります。 約束のようなマイクロタスクは、のようなマクロタスクよりも高い優先度で処理されます setTimeout.

これで、イベントループがスタックとキューを使用してコードの実行順序を処理する方法がわかりました。 次のタスクは、コードの実行順序を制御する方法を理解することです。 これを行うには、最初に、非同期コードがイベントループによって正しく処理されるようにする元の方法であるコールバック関数について学習します。

コールバック関数

の中に setTimeout たとえば、タイムアウトのある関数は、メインのトップレベルの実行コンテキストのすべての後に実行されました。 ただし、次のような機能の1つを確保したい場合は third 関数はタイムアウト後に実行され、非同期コーディングメソッドを使用する必要があります。 ここでのタイムアウトは、データを含む非同期API呼び出しを表すことができます。 API呼び出しからのデータを処理したいが、データが最初に返されることを確認する必要があります。

この問題に対処するための元の解決策は、コールバック関数を使用することです。 コールバック関数には特別な構文はありません。 これらは、引数として別の関数に渡された関数にすぎません。 別の関数を引数とする関数を高階関数と呼びます。 この定義によれば、引数として渡された場合、任意の関数がコールバック関数になる可能性があります。 コールバックは本質的に非同期ではありませんが、非同期の目的で使用できます。

高階関数とコールバックの構文コードの例を次に示します。

// A function
function fn() {
  console.log('Just a function')
}

// A function that takes another function as an argument
function higherOrderFunction(callback) {
  // When you call a function that is passed as an argument, it is referred to as a callback
  callback()
}

// Passing a function
higherOrderFunction(fn)

このコードでは、関数を定義します fn、関数を定義する higherOrderFunction それは機能を取ります callback 引数として、そして渡す fn へのコールバックとして higherOrderFunction.

このコードを実行すると、次のようになります。

Output
Just a function

に戻りましょう first, second、 と third で機能する setTimeout. これはあなたがこれまでに持っているものです:

function first() {
  console.log(1)
}

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

function third() {
  console.log(3)
}

タスクは取得することです third の非同期アクションが終了するまで実行を常に遅らせる関数 second 機能が完了しました。 これがコールバックの出番です。 実行する代わりに first, second、 と third 実行のトップレベルで、あなたは合格します third の引数として機能する second. The second 関数は、非同期アクションが完了した後にコールバックを実行します。

コールバックが適用された3つの関数は次のとおりです。

// Define three functions
function first() {
  console.log(1)
}

function second(callback) {
  setTimeout(() => {
    console.log(2)

    // Execute the callback function
    callback()
  }, 0)
}

function third() {
  console.log(3)
}

今、実行します firstsecond、次に合格 third の議論として second:

first()
second(third)

このコードブロックを実行すると、次の出力が表示されます。

Output
1 2 3

初め 1 が印刷され、タイマーが完了すると(この場合はゼロ秒ですが、任意の量に変更できます)、印刷されます。 2 それから 3. 関数をコールバックとして渡すことにより、非同期Web API((setTimeout)完了します。

ここで重要なポイントは、コールバック関数が非同期ではないということです。setTimeout 非同期タスクの処理を担当する非同期WebAPIです。 コールバックを使用すると、非同期タスクが完了したときに通知を受け、タスクの成功または失敗を処理できます。

コールバックを使用して非同期タスクを処理する方法を学習したので、次のセクションでは、あまりにも多くのコールバックをネストして「運命のピラミッド」を作成する問題について説明します。

ネストされたコールバックと運命のピラミッド

コールバック関数は、別の関数が完了してデータを返すまで、関数の実行を遅らせるための効果的な方法です。 ただし、コールバックはネストされているため、相互に依存する連続した非同期リクエストが多数ある場合、コードが乱雑になる可能性があります。 これは、JavaScript開発者にとって初期の大きなフラストレーションであり、その結果、ネストされたコールバックを含むコードは、「運命のピラミッド」または「コールバック地獄」と呼ばれることがよくあります。

ネストされたコールバックのデモンストレーションは次のとおりです。

function pyramidOfDoom() {
  setTimeout(() => {
    console.log(1)
    setTimeout(() => {
      console.log(2)
      setTimeout(() => {
        console.log(3)
      }, 500)
    }, 2000)
  }, 1000)
}

このコードでは、それぞれの新しい setTimeout 高階関数内にネストされ、より深いコールバックのピラミッド形状を作成します。 このコードを実行すると、次のようになります。

Output
1 2 3

実際には、実際の非同期コードでは、これははるかに複雑になる可能性があります。 ほとんどの場合、非同期コードでエラー処理を実行してから、各応答から次の要求にデータを渡す必要があります。 コールバックを使用してこれを行うと、コードの読み取りと保守が困難になります。

これは、より現実的な「運命のピラミッド」の実行可能な例です。

// Example asynchronous function
function asynchronousRequest(args, callback) {
  // Throw an error if no arguments are passed
  if (!args) {
    return callback(new Error('Whoa! Something went wrong.'))
  } else {
    return setTimeout(
      // Just adding in a random number so it seems like the contrived asynchronous function
      // returned different data
      () => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
      500,
    )
  }
}

// Nested asynchronous requests
function callbackHell() {
  asynchronousRequest('First', function first(error, response) {
    if (error) {
      console.log(error)
      return
    }
    console.log(response.body)
    asynchronousRequest('Second', function second(error, response) {
      if (error) {
        console.log(error)
        return
      }
      console.log(response.body)
      asynchronousRequest(null, function third(error, response) {
        if (error) {
          console.log(error)
          return
        }
        console.log(response.body)
      })
    })
  })
}

// Execute 
callbackHell()

このコードでは、すべての関数アカウントを可能にする必要があります response そして可能性 error、関数を作成します callbackHell 視覚的に混乱します。

このコードを実行すると、次のようになります。

Output
First 9 Second 3 Error: Whoa! Something went wrong. at asynchronousRequest (<anonymous>:4:21) at second (<anonymous>:29:7) at <anonymous>:9:13

非同期コードを処理するこの方法に従うのは困難です。 その結果、promisesの概念がES6に導入されました。 これが次のセクションの焦点です。

約束

promise は、非同期機能の完了を表します。 将来的に値を返す可能性のあるオブジェクトです。 コールバック関数と同じ基本的な目標を達成しますが、多くの追加機能とより読みやすい構文を備えています。 JavaScript開発者は、プロミスを作成するよりも多くの時間を費やす可能性があります。これは、通常、開発者が消費するプロミスを返す非同期WebAPIであるためです。 このチュートリアルでは、両方を行う方法を説明します。

約束を作成する

あなたはで約束を初期化することができます new Promise 構文であり、関数で初期化する必要があります。 promiseに渡される関数には resolvereject パラメーター。 The resolvereject 関数は、それぞれ操作の成功と失敗を処理します。

約束を宣言するには、次の行を記述します。

// Initialize a promise
const promise = new Promise((resolve, reject) => {})

この状態で初期化されたpromiseをWebブラウザーのコンソールで調べると、 pending ステータスと undefined 価値:

Output
__proto__: Promise [[PromiseStatus]]: "pending" [[PromiseValue]]: undefined

これまでのところ、約束のために何も設定されていないので、そこに座って pending 永遠に状態。 約束をテストするために最初にできることは、値でそれを解決することによって約束を果たすことです。

const promise = new Promise((resolve, reject) => {
  resolve('We did it!')
})

さて、約束を調べると、あなたはそれが次のステータスを持っていることがわかります fulfilled、および value 渡した値に設定します resolve:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: "We did it!"

このセクションの冒頭で述べたように、promiseは値を返す可能性のあるオブジェクトです。 正常に実行された後、 value から行く undefined データが入力されます。

約束には、保留中、履行済み、および拒否の3つの状態があります。

履行または拒否された後、約束は解決されます。

これで、Promiseがどのように作成されるかがわかったので、開発者がこれらのPromiseをどのように使用するかを見てみましょう。

約束を消費する

前のセクションの約束は値で満たされましたが、値にアクセスできるようにする必要もあります。 Promiseには次のメソッドがあります then それは約束が達した後に実行されます resolve コード内。 then promiseの値をパラメーターとして返します。

これは、戻ってログに記録する方法です。 value 例の約束の:

promise.then((response) => {
  console.log(response)
})

あなたが作成した約束には [[PromiseValue]]We did it!. この値は、匿名関数に次のように渡される値です。 response:

Output
We did it!

これまでのところ、作成した例には非同期Web APIは含まれていませんでした。これは、ネイティブJavaScript Promiseを作成、解決、および使用する方法のみを説明したものです。 使用する setTimeout、非同期リクエストをテストできます。

次のコードは、非同期リクエストから返されるデータをpromiseとしてシミュレートします。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Resolving an asynchronous request!'), 2000)
})

// Log the result
promise.then((response) => {
  console.log(response)
})

を使用して then 構文により、 response 次の場合にのみログに記録されます setTimeout 操作が完了した後 2000 ミリ秒。 これはすべて、コールバックをネストせずに実行されます。

2秒後、promise値が解決され、ログインされます。 then:

Output
Resolving an asynchronous request!

Promiseをチェーン化して、データを複数の非同期操作に渡すこともできます。 に値が返された場合 then、 別 then 前の戻り値で満たされる追加が可能 then:

// Chain a promise
promise
  .then((firstResponse) => {
    // Return a new value for the next then
    return firstResponse + ' And chaining!'
  })
  .then((secondResponse) => {
    console.log(secondResponse)
  })

2番目の満たされた応答 then 戻り値をログに記録します。

Output
Resolving an asynchronous request! And chaining!

以来 then 連鎖させることができ、ネストする必要がないため、promiseの消費がコールバックよりも同期的に見えるようになります。 これにより、より読みやすいコードが可能になり、保守と検証が容易になります。

エラー処理

これまでのところ、あなたは成功した約束を処理しただけです resolve、約束を fulfilled 州。 ただし、非同期リクエストでは、APIがダウンしている場合や、不正な形式または不正なリクエストが送信された場合に、エラーを処理する必要があることがよくあります。 約束は両方の場合を処理できるはずです。 このセクションでは、promiseの作成と使用の成功とエラーの両方のケースをテストする関数を作成します。

これ getUsers 関数はpromiseにフラグを渡し、promiseを返します。

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Handle resolve and reject in the asynchronous API
    }, 1000)
  })
}

次のようにコードを設定します onSuccesstrue、タイムアウトはいくつかのデータで満たされます。 もしも false、関数はエラーで拒否します:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Handle resolve and reject in the asynchronous API
      if (onSuccess) {
        resolve([
          {id: 1, name: 'Jerry'},
          {id: 2, name: 'Elaine'},
          {id: 3, name: 'George'},
        ])
      } else {
        reject('Failed to fetch data!')
      }
    }, 1000)
  })
}

正常な結果を得るには、サンプルユーザーデータを表すJavaScriptオブジェクトを返します。

エラーを処理するために、を使用します catch インスタンスメソッド。 これにより、失敗のコールバックが発生します error パラメータとして。

を実行します getUser でコマンド onSuccess に設定 false、を使用して then 成功事例と catch エラーの方法:

// Run the getUsers function with the false flag to trigger an error
getUsers(false)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

エラーがトリガーされたため、 then スキップされ、 catch エラーを処理します:

Output
Failed to fetch data!

フラグを切り替えて resolve 代わりに、 catch は無視され、代わりにデータが返されます。

// Run the getUsers function with the true flag to resolve successfully
getUsers(true)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

これにより、ユーザーデータが生成されます。

Output
(3) [{…}, {…}, {…}] 0: {id: 1, name: "Jerry"} 1: {id: 2, name: "Elaine"} 3: {id: 3, name: "George"}

参考までに、ハンドラーメソッドを含む表を次に示します。 Promise オブジェクト:

方法 説明
then() を処理します resolve. 約束を返し、電話します onFulfilled 非同期で機能する
catch() を処理します reject. 約束を返し、電話します onRejected 非同期で機能する
finally() 約束が決まったときに呼び出されます。 約束を返し、電話します onFinally 非同期で機能する

これまで非同期環境で作業したことがない新しい開発者と経験豊富なプログラマーの両方にとって、約束は混乱を招く可能性があります。 ただし、前述のように、プロミスを作成するよりも消費する方がはるかに一般的です。 通常、ブラウザのWeb APIまたはサードパーティのライブラリが約束を提供し、それを使用するだけで済みます。

最後のpromiseセクションでは、このチュートリアルで、promiseを返すWeb APIの一般的なユースケースを引用します: FetchAPI

PromisesでのFetchAPIの使用

promiseを返す最も便利で頻繁に使用されるWebAPIの1つは、Fetch APIです。これにより、ネットワークを介して非同期リソース要求を行うことができます。 fetch は2つの部分からなるプロセスであるため、連鎖が必要です then. この例は、GitHub APIをヒットしてユーザーのデータをフェッチすると同時に、潜在的なエラーを処理する方法を示しています。

// Fetch a user from the GitHub API
fetch('https://api.github.com/users/octocat')
  .then((response) => {
    return response.json()
  })
  .then((data) => {
    console.log(data)
  })
  .catch((error) => {
    console.error(error)
  })

The fetch リクエストはに送信されます https://api.github.com/users/octocat 非同期で応答を待機するURL。 最初 then 応答をJSONデータとしてフォーマットする無名関数に応答を渡し、次にJSONを秒に渡します then データをコンソールに記録します。 The catch ステートメントは、エラーをコンソールに記録します。

このコードを実行すると、次のようになります。

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

これはから要求されたデータです https://api.github.com/users/octocat、JSON形式でレンダリングされます。

チュートリアルのこのセクションでは、promiseに非同期コードを処理するための多くの改善が組み込まれていることを示しました。 しかし、使用している間 then 非同期アクションを処理することは、コールバックのピラミッドよりも簡単ですが、一部の開発者は、非同期コードを記述する同期形式を依然として好みます。 このニーズに対応するために、 ECMAScript 2016(ES7)が導入されました async 関数と await 約束の操作を簡単にするキーワード。

非同期関数 async/await

非同期関数を使用すると、同期しているように見える方法で非同期コードを処理できます。 async 関数はまだ内部でpromiseを使用しますが、より伝統的なJavaScript構文を持っています。 このセクションでは、この構文の例を試してみます。

あなたは作成することができます async を追加して機能 async 関数の前のキーワード:

// Create an async function
async function getUser() {
  return {}
}

この関数はまだ非同期を処理していませんが、従来の関数とは動作が異なります。 関数を実行すると、promiseが返されることがわかります。 [[PromiseStatus]][[PromiseValue]] 戻り値の代わりに。

への呼び出しをログに記録して、これを試してください getUser 関数:

console.log(getUser())

これにより、次のようになります。

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: Object

これはあなたが扱うことができることを意味します async で機能する then 同じようにあなたは約束を処理することができます。 次のコードでこれを試してください。

getUser().then((response) => console.log(response))

この呼び出しは getUser 戻り値を、値をコンソールに記録する無名関数に渡します。

このプログラムを実行すると、次のメッセージが表示されます。

Output
{}

アン async 関数は、関数内で呼び出されたpromiseを処理できます。 await オペレーター。 await 内で使用することができます async 関数であり、promiseが解決するまで待機してから、指定されたコードを実行します。

この知識があれば、前のセクションのFetchリクエストを次のように書き直すことができます。 async/await 次のように:

// Handle fetch with async/await
async function getUser() {
  const response = await fetch('https://api.github.com/users/octocat')
  const data = await response.json()

  console.log(data)
}

// Execute async function
getUser()

The await ここの演算子は、 data リクエストがデータを入力する前にログに記録されません。

さて、決勝 data 内部で処理することができます getUser 機能、使用する必要はありません then. これはロギングの出力です data:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

注:多くの環境では、 async 使用する必要があります await—ただし、一部の新しいバージョンのブラウザとノードでは、トップレベルの使用が許可されています await、これにより、非同期関数の作成をバイパスして、 await の。

最後に、非同期関数内で実行されたpromiseを処理しているため、関数内でエラーを処理することもできます。 を使用する代わりに catch との方法 then try /catchパターンを使用して例外を処理します。

次の強調表示されたコードを追加します。

// Handling success and errors with async/await
async function getUser() {
  try {
    // Handle success in try
    const response = await fetch('https://api.github.com/users/octocat')
    const data = await response.json()

    console.log(data)
  } catch (error) {
    // Handle error in catch
    console.error(error)
  }
}

プログラムはスキップします catch エラーを受け取った場合はブロックし、そのエラーをコンソールに記録します。

最新の非同期JavaScriptコードは、ほとんどの場合、 async/await 構文ですが、Promiseがどのように機能するかについて実用的な知識を持っていることが重要です。特に、Promiseは、では処理できない追加機能を実行できるためです。 async/await、promiseを Promise.all()と組み合わせるようなものです。

注: async/await ジェネレーターをpromiseと組み合わせて使用することで再現でき、コードに柔軟性を追加できます。 詳細については、JavaScriptでのジェネレーターの理解チュートリアルをご覧ください。

結論

Web APIはデータを非同期で提供することが多いため、非同期アクションの結果を処理する方法を学ぶことは、JavaScript開発者であるための重要な部分です。 この記事では、ホスト環境がイベントループを使用して、スタックおよびキューでコードの実行順序を処理する方法を学習しました。 また、コールバック、Promise、およびを使用して、非同期イベントの成功または失敗を処理する3つの方法の例を試しました。 async/await 構文。 最後に、FetchWebAPIを使用して非同期アクションを処理しました。

ブラウザが並列イベントを処理する方法の詳細については、MozillaDeveloperNetworkの同時実行モデルとイベントループを参照してください。 JavaScriptについて詳しく知りたい場合は、JavaScriptでコーディングする方法シリーズに戻ってください。

モバイルバージョンを終了