
ほぼすべての有用なJSプログラムは、非同期開発メソッドを使用して記述されています。 ここで、コールバック関数が口語的に作用します-「コールバック」。 これは、Promise、または通常Promiseと呼ばれるPromiseオブジェクトが使用される場所です。 ここで、ジェネレーターとasync / awaitコンストラクトを実行できます。 非同期コードは、同期と比較して、通常、書き込み、読み取り、および保守が困難です。 コールバック地獄のような完全に不気味な構造に変わることもあります。 しかし、それなしではできません。
今日は、コールバック、プロミス、ジェネレーター、async / awaitコンストラクトの機能について説明し、シンプルで理解しやすく、効率的な非同期コードの書き方を考えます。
同期および非同期コードについて
同期JSコードと非同期JSコードのフラグメントを見てみましょう。 たとえば、通常の同期コードは次のとおりです。
console.log('1') console.log('2') console.log('3')
彼は、それほど困難なく、コンソールに1から3までの数字を表示します。
これでコードは非同期になりました:
console.log('1') setTimeout(function afterTwoSeconds() { console.log('2') }, 2000) console.log('3')
ここでは、シーケンス1、3、2がすでに表示されています番号2は、
setTimeout
関数が呼び出されたときに設定されたタイマーイベントを処理するコールバックから派生しています。 この例では、2秒後にコールバックが呼び出されます。 アプリケーションは停止せず、これらの2秒が経過するのを待ちます。 代わりに、実行が継続され、タイマーが切れると、
afterTwoSeconds
関数が呼び出されます。
たぶん、あなたがJS開発者として始めたばかりなら、あなたは自問するかもしれません。 おそらく非同期コードを同期に再作成することは可能ですか?」 これらの質問に対する答えを探しましょう。
問題の声明
GitHubユーザーを見つけて、彼のリポジトリに関するデータをダウンロードするタスクに直面しているとします。 ここでの主な問題は、正確なユーザー名がわからないことです。そのため、探しているものとそのリポジトリに似た名前を持つすべてのユーザーをリストする必要があります。
インターフェイスに関しては、 単純なものに制限しています。

GitHubユーザーおよび対応するリポジトリ用のシンプルな検索インターフェイス
例では、クエリは
XMLHttpRequest
(XHR)を使用して実行されますが、jQuery(
$.ajax
)、または
fetch
関数の使用に基づくより現代的な標準アプローチを使用できます。 これらは両方とも、約束の使用に要約されます。 コードは、旅行に応じて変更されますが、最初の例では、この例は次のとおりです。
// url - 'https://api.github.com/users/daspinola/repos' function request(url) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { // } else { // } } } xhr.ontimeout = function () { // , , } xhr.open('get', url, true) xhr.send(); }
これらの例では、最終的にサーバーから得られるものやその処理方法ではなく、非同期設計で使用できるさまざまなアプローチを使用したコード自体の編成が重要であることに注意してください。
コールバック関数
JSの関数では、他の関数への引数として渡すなど、多くのことができます。 通常、これは、何らかのプロセスの完了後に転送された関数を呼び出すために行われますが、これには時間がかかる場合があります。 コールバック関数についてです。 以下に簡単な例を示します。
// "doThis" , - "andThenThis". "doThis" , , , , "andThenThis". doThis(andThenThis) // "doThis" "callback" , , , function andThenThis() { console.log('and then this') } // , , , "callback" - function doThis(callback) { console.log('this first') // , , , , , '()', callback() }
このアプローチを使用して問題を解決するために、次の
request
関数を作成できます。
function request(url, callback) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { callback(null, xhr.response) } else { callback(xhr.status, null) } } } xhr.ontimeout = function () { console.log('Timeout') } xhr.open('get', url, true) xhr.send(); }
現在、リクエストを実行するための関数は
callback
パラメータを受け入れるため、リクエストを実行してサーバーレスポンスを受信した後、エラーが発生した場合、および操作が正常に完了した場合にコールバックが呼び出されます。
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` request(userGet, function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) { request(user.repos_url, function handleReposList(err, repos) { if (err) throw err // }) }) })
ここで何が起こるかを分析しましょう:
- ユーザーリポジトリを取得するためのリクエストが実行されます(この場合、自分のリポジトリをロードします)。
- 要求が完了すると、
handleUsersList
handleUsersListがhandleUsersList
ます。
- エラーがなかった場合、J
SON.parse
を使用してサーバーの応答を解析し、便宜上、オブジェクトに変換します。
- その後、ユーザーのリストをソートします。複数の要素が含まれている可能性があるため、各ユーザーに対して、最初のリクエスト後に各ユーザーに返されたURLを使用してリポジトリのリストをリクエストします。 これは、
repos_url
が次のリクエストのURLであり、最初のリクエストから取得したことを意味します。
- リポジトリデータをダウンロードするリクエストが完了すると、コールバックが呼び出されます。現在は
handleReposList
です。 ここでは、ユーザーのリストをロードするときと同じように、ユーザーリポジトリのリストを含むエラーまたは有用なデータを処理できます。
オブジェクトの最初のパラメーターとしてエラーを使用することは、特にNode.jsを使用した開発では広く行われていることに注意してください。
コードをより完全に見て、エラー処理ツールを装備し、コールバック関数の定義をクエリ実行コードから分離すると、プログラムの可読性が向上します。次のようになります。
try { request(userGet, handleUsersList) } catch (e) { console.error('Request boom! ', e) } function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) { request(user.repos_url, handleReposList) }) } function handleReposList(err, repos) { if (err) throw err // console.log('My very few repos', repos) }
このアプローチは機能しますが、それを使用すると、クエリレースの状態やエラー処理の問題などの問題が発生するリスクがあります。 ただし、コールバックに関連する主な迷惑は、
forEach
ループで何が起こるかを考慮すると、ここでは3つですが、そのようなコードは読みにくく、保守が難しいということです。 同様の問題は、おそらくコールバック関数が登場した日から存在します;それはコールバック地獄として広く知られています。

すべての栄光の地獄コールバック。 ここから撮影した画像。
この場合、「競合状態」とは、ユーザーリポジトリに関するデータを取得する手順を制御しない状況を意味します。 私たちはすべてのユーザーのデータを要求しますが、これらの要求への回答が混在していることが判明する場合があります。 10番目のユーザーに対する答えが最初に来て、2番目のユーザーに対する答えが最後だとしましょう。 以下に、この問題の可能な解決策について説明します。
約束
promiseを使用すると、コードが読みやすくなります。 その結果、たとえば、新しい開発者がプロジェクトに来た場合、彼はすべてがそこでどのように配置されているかをすぐに理解します。
約束を作成するには、次の設計を使用できます。
const myPromise = new Promise(function(resolve, reject) { // if (codeIsFine) { resolve('fine') } else { reject('error') } }) myPromise .then(function whenOk(response) { console.log(response) return response }) .catch(function notOk(err) { console.error(err) })
この例を見てみましょう:
- promiseは、
resolve
メソッドとreject
メソッドの呼び出しがある関数を使用して初期化されます。
- 非同期コードは、
Promise
コンストラクターを使用して作成された関数内に配置されます。 コードが成功した場合は、resolve
メソッドが呼び出され、そうでない場合はreject
が呼び出されます。
- 関数が
resolve
呼び出すと、Promise
オブジェクトの.then
メソッドが実行されます。同様に、reject
が呼び出されると、.catch
メソッドが実行されます。
約束を処理する際に覚えておくべきことがいくつかあります。
-
resolve
およびreject
メソッドは、結果として、たとえば、タイプresolve('yey', 'works')
コマンドを実行する場合、1つのパラメーターのみを受け入れ'yey'
。
- 複数の
.then
呼び出しをチェーンにチェーンする場合は、対応するコールバックの最後に常にreturn
を使用する必要があります。そうしないと、それらはすべて同時に実行されます。これは明らかに達成したいことではありません。
-
reject
コマンドが実行されたときに、チェーンの次が.then
である場合、それが実行されます(.then
はとにかく実行される式と見なすことができます)。
- いずれかの
.then
呼び出しのチェーンでエラーが発生した場合、それに続くものは.catch
式が.catch
までスキップ.catch
ます。
- Promiseには3つの状態があります。「保留」-
resolve
またはreject
呼び出しの待機状態、および「resolved」および「rejected」の状態。これは、成功、resolve
呼び出し、および失敗の呼び出し、約束の作業の終了に対応します。 プロミスが解決済みまたは拒否済みの状態にある場合、それは変更できなくなります。
別々に定義された関数を使用せずにプロミスを作成できることに注意してください。プロミスの作成時の関数を記述します。 この例で示されているのは、Promiseを初期化する一般的な方法です。
理論上でふらつかないように、例に戻りましょう。 promiseを使用して書き換えます。
function request(url) { return new Promise(function (resolve, reject) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.response) } else { reject(xhr.status) } } } xhr.ontimeout = function () { reject('timeout') } xhr.open('get', url, true) xhr.send(); }) }
このアプローチでは、
request
を呼び出すと、次のようなものが返されます。

これは保留中の約束です。 正常に解決するか拒否することができます。
次に、新しい
request
関数を使用して、残りのコードを書き直します。
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const myPromise = request(userGet) console.log('will be pending when logged', myPromise) myPromise .then(function handleUsersList(users) { console.log('when resolve is found it comes here with the response, in this case users ', users) const list = JSON.parse(users).items return Promise.all(list.map(function(user) { return request(user.repos_url) })) }) .then(function handleReposList(repos) { console.log('All users repos in an array', repos) }) .catch(function handleErrors(error) { console.log('when a reject is executed it will come here ignoring the then statement ', error) })
ここでは、最初の表現に自分自身を見つけ、
.then
約束の解決に成功しました。 ユーザーのリストがあります。 2番目の
.then
式では、リポジトリに配列を渡します。 何かがうまくいかなかった場合、
.catch
式になります。
このアプローチのおかげで、私たちはレースの状態と、そうすることで生じるいくつかの問題を把握しました。 ここではコールバックの地獄は観察されませんが、コードはまだそれほど読みやすくありません。 実際、この例では、コールバック関数の宣言を強調表示することにより、さらに改善することができます。
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const userRequest = request(userGet) // , userRequest .then(handleUsersList) .then(repoRequest) .then(handleReposList) .catch(handleErrors) function handleUsersList(users) { return JSON.parse(users).items } function repoRequest(users) { return Promise.all(users.map(function(user) { return request(user.repos_url) })) } function handleReposList(repos) { console.log('All users repos in an array', repos) } function handleErrors(error) { console.error('Something went wrong ', error) }
このアプローチでは、
.then
式のコールバック名を見ると、
userRequest
を呼び出す意味が
userRequest
ます。 コードは扱いやすく、読みやすいです。
実際、これは約束と呼ばれるものの氷山の一角にすぎません。 このトピックをより徹底的に掘り下げたい人に読むことをお勧めする資料があります。
発電機
私たちの問題を解決する別のアプローチは、しかし、あなたが頻繁に出会うことはないでしょうが、ジェネレーターです。 このトピックは他のトピックよりも少し複雑なので、勉強するには時期尚早だと感じた場合は、この資料の次のセクションにすぐに進むことができます。
ジェネレーター関数を定義するには、キーワード
function
後にアスタリスク「*」を使用できます。 ジェネレーターを使用すると、非同期コードを同期に非常に似たものにすることができます。 たとえば、次のようになります。
function* foo() { yield 1 const args = yield 2 console.log(args) } var fooIterator = foo() console.log(fooIterator.next().value) // 1 console.log(fooIterator.next().value) // 2 fooIterator.next('aParam') // console.log 'aParam'
ここでのポイントは、ジェネレーターが
return
代わりに
yield
式を使用し、次の
.next
イテレーターの呼び出しまで関数の実行を停止することです。 これは、promiseを解決するときに実行されるpromiseの
.then
式に似ています。
次に、これらすべてをタスクに適用する方法を見てみましょう。
request
関数は次のとおりです。
function request(url) { return function(callback) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { callback(null, xhr.response) } else { callback(xhr.status, null) } } } xhr.ontimeout = function () { console.log('timeout') } xhr.open('get', url, true) xhr.send() } }
ここでは、いつものように
url
引数を使用しますが、リクエストをすぐに実行するのではなく、レスポンスを処理するコールバック関数がある場合にのみリクエストを実行します。
ジェネレータは次のようになります。
function* list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = yield request(userGet) yield for (let i = 0; i<=users.length; i++) { yield request(users[i].repos_url) } }
ここで何が起こるかです:
- 関数へのリンクを返し、この最初のリクエストのコールバックを待機することで、最初のリクエストが準備されることを期待します(
url
を受け入れ、コールバックを期待する関数を返すrequest
関数を思い出してください)。
- ユーザーのリストusersが次の
.next
送信されるのを.next
ます。
- 結果の
users
配列を.next
、それぞれに対して.next
が対応するコールバックを返すことを期待します。
これをすべて使用すると、次のようになります。
try { const iterator = list() iterator.next().value(function handleUsersList(err, users) { if (err) throw err const list = JSON.parse(users).items // iterator.next(list) list.forEach(function(user) { iterator.next().value(function userRepos(error, repos) { if (error) throw repos // console.log(user, JSON.parse(repos)) }) }) }) } catch (e) { console.error(e) }
ここで、各ユーザーのリポジトリのリストを個別に処理できます。 このコードを改善するために、すでに上で行ったように、コールバック関数を区別できます。
ジェネレーターについてはあいまいです。 一方では、ジェネレーターを見ることでコードに何を期待するかをすぐに理解できます。他方では、ジェネレーターの実行はコールバック地獄で発生する問題と同様の問題につながります。
ジェネレーターは比較的新しい機能であるため、古いバージョンのブラウザーでコードの使用を期待している場合、トランスパイラーでコードを処理する必要があることに注意してください。 さらに、非同期コードを記述するジェネレーターはあまり使用されないため、チーム開発に関与している場合、一部のプログラマーはそれらに慣れていない可能性があることに注意してください。
その場合、このトピックをよりよく理解することにした場合、ジェネレーターの内部構造に関する優れた資料があります。
非同期/待機
この方法は、ジェネレーターとプロミスの混合に似ています。 非同期に実行される関数を
async
で指定し、
await
を使用し
await
、対応するプロミスが解決されるのをコードのどの部分で待つかをシステムに指示するだけです。
いつものように、最初に簡単な例を示します。
sumTwentyAfterTwoSeconds(10) .then(result => console.log('after 2 seconds', result)) async function sumTwentyAfterTwoSeconds(value) { const remainder = afterTwoSeconds(20) return value + await remainder } function afterTwoSeconds(value) { return new Promise(resolve => { setTimeout(() => { resolve(value) }, 2000); }); }
ここで次のことが起こります。
- 非同期関数
sumTwentyAfterTwoSeconds
ます。
- コードはafterTwoSeconds
afterTwoSeconds
解決を待つことをお勧めreject
ます。
- コードの実行は
.then
で終了し、そこでawait
キーワードでマークされた操作が終了します。この場合、それは1つの操作です。
async/await
コンストラクトで使用する
request
関数を準備します。
function request(url) { return new Promise(function(resolve, reject) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.response) } else { reject(xhr.status) } } } xhr.ontimeout = function () { reject('timeout') } xhr.open('get', url, true) xhr.send() }) }
async
を使用し
await
関数を作成します。ここで
await
キーワードを使用します。
async function list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = await request(userGet) const usersList = JSON.parse(users).items usersList.forEach(async function (user) { const repos = await request(user.repos_url) handleRepoList(user, repos) }) } function handleRepoList(user, repos) { const userRepos = JSON.parse(repos) // console.log(user, userRepos) }
そのため、リクエストを処理する非同期
list
関数があります。 また、リポジトリのリストを作成するには、
forEach
ループに
async/await
コンストラクトが必要です。 すべてを呼び出すのは非常に簡単です。
list() .catch(e => console.error(e))
このアプローチとプロミスの使用は、非同期プログラミングの私のお気に入りの方法です。 それらを使用して記述されたコードは、読み取りや編集に便利です。
async/await
詳細については、 こちらをご覧ください 。
async/await
のマイナス、およびジェネレーターのマイナスは、この設計が古いブラウザでサポートされていないことです。サーバー開発で使用するには、ノード8を使用する必要があります。
まとめ
ここで、
async/await
を使用して、マテリアルの先頭にある問題を解決するプロジェクトコードを確認できます。 話したことを適切に処理したい場合は、このコードと、説明したすべてのテクノロジーを試してください。
$.ajax
や
fetch
などの別のクエリ実行方法を使用して例を書き換えると、例が改善され簡潔になることに注意してください。 上記の方法を使用してコードの品質を改善する方法についてアイデアをお持ちの場合、それについて教えていただければ幸いです。
割り当てられたタスクの詳細に応じて、非同期/待機、コールバック、または異なるテクノロジーの混合を使用することが判明する場合があります。 実際、どの非同期開発手法を選択するかという質問に対する答えは、プロジェクトの機能によって異なります。 アプローチが、あなたと他のチームメンバーにとって理解しやすい(そして、しばらくすると明らかになる)保守しやすい読み取り可能なコードを使用して問題を解決できる場合、このアプローチが必要です。
親愛なる読者! 非同期JavaScriptコードを記述するためにどのようなテクニックを使用しますか?