→さまざまな例での非同期動作のより一般的な説明について は、関数内で変数を変更した後、変数が変更されないのはなぜですかを参照してください。-非同期コードリファレンス
→すでに問題を理解している場合は、以下の可能な解決策に進んでください。
問題
AjaxのAは非同期を意味します。つまり、要求の送信(または応答の受信)は、通常の実行フローから除外されます。この例では、すぐに戻り、コールバックとして渡された関数が呼び出される前に次のステートメントが実行されます。$.ajax
return result;
success
これは、同期フローと非同期フローの違いを明確にしてくれる類推です。
同期
あなたが友人に電話をかけ、彼にあなたのために何かを探すように頼むと想像してください。少し時間がかかるかもしれませんが、友人があなたに必要な答えをくれるまで、電話で待って宇宙を見つめます。
「通常の」コードを含む関数呼び出しを行った場合も同様です。
function findItem() {
var item;
while(item_not_found) {
// search
}
return item;
}
var item = findItem();
// Do something with item
doSomethingElse();
findItem
実行には長い時間がかかる場合がありますが、その後に続くコードvar item = findItem();
は、関数が結果を返すまで待機する必要があります。
非同期
同じ理由で友達にもう一度電話をかけます。しかし、今回はあなたが急いでいるので、彼はあなたの携帯電話にかけ直すべきだと彼に言います。あなたは電話を切り、家を出て、あなたが計画していたことを何でもします。あなたの友人があなたに電話をかけたら、あなたは彼があなたに与えた情報を扱います。
これがまさに、Ajaxリクエストを実行したときに起こっていることです。
findItem(function(item) {
// Do something with item
});
doSomethingElse();
応答を待つのではなく、実行は直ちに続行され、Ajax呼び出しの後のステートメントが実行されます。最終的に応答を取得するには、応答が受信されたときに呼び出される関数、コールバック(何か通知?コールバック?)を提供します。その呼び出しの後に続くステートメントは、コールバックが呼び出される前に実行されます。
ソリューション
JavaScriptの非同期性を受け入れてください!特定の非同期操作は同期対応を提供します(「Ajax」も同様)。ただし、特にブラウザーのコンテキストでは、それらを使用することはお勧めしません。
なぜ悪いのですか?
JavaScriptはブラウザーのUIスレッドで実行され、長時間実行されるプロセスはUIをロックし、応答しなくなります。さらに、JavaScriptの実行時間には上限があり、ブラウザーは実行を続行するかどうかをユーザーに尋ねます。
これらはすべて、ユーザーエクスペリエンスが非常に悪いものです。ユーザーは、すべてが正常に機能しているかどうかを確認できません。さらに、接続が遅いユーザーにとっては、この影響はさらに大きくなります。
以下では、3つの異なるソリューションが互いに重なり合っていることを確認します。
- 約束
async/await
(ES2017 +、トランスパイラーまたは再生器を使用する場合は古いブラウザーで使用可能)
- コールバック(ノードで人気)
- Promises with
then()
(ES2015 +、多くのpromiseライブラリーのいずれかを使用する場合、古いブラウザーで使用可能)
3つすべては、現在のブラウザーとノード7以降で使用できます。
2017年にリリースされたECMAScriptバージョンでは、非同期関数の構文レベルのサポートが導入されました。助けを借りてasync
とawait
、あなたは「同期型」で非同期を書くことができます。コードはまだ非同期ですが、読みやすく、理解しやすくなっています。
async/await
async
promiseの上に構築します。関数は常にpromiseを返します。await
プロミスを「アンラップ」し、プロミスが解決された値になるか、プロミスが拒否された場合はエラーをスローします。
重要:関数await
内でのみ使用できasync
ます。現在、トップレベルawait
はまだサポートされていないため、コンテキストを開始するために非同期IIFE(即時に呼び出される関数式)を作成する必要がある場合がありasync
ます。
MDNの詳細についてはasync
、こちらをご覧くださいawait
。
上記の遅延の上に構築する例を次に示します。
// Using 'superagent' which will return a promise.
var superagent = require('superagent')
// This is isn't declared as `async` because it already returns a promise
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
async function getAllBooks() {
try {
// GET a list of book IDs of the current user
var bookIDs = await superagent.get('/user/books');
// wait for 3 seconds (just for the sake of this example)
await delay();
// GET information about each book
return await superagent.get('/books/ids='+JSON.stringify(bookIDs));
} catch(error) {
// If any of the awaited promises was rejected, this catch block
// would catch the rejection reason
return null;
}
}
// Start an IIFE to use `await` at the top level
(async function(){
let books = await getAllBooks();
console.log(books);
})();
現在のブラウザとノードのバージョンがサポートしていasync/await
ます。リジェネレーター(またはBabelなどのリジェネレーターを使用するツール)を使用してコードをES5に変換することにより、古い環境をサポートすることもできます。
関数にコールバックを受け入れさせる
コールバックは、別の関数に渡される関数です。その他の関数は、準備ができるといつでも渡された関数を呼び出すことができます。非同期プロセスのコンテキストでは、非同期プロセスが完了するたびにコールバックが呼び出されます。通常、結果はコールバックに渡されます。
質問の例ではfoo
、コールバックを受け入れてコールバックとして使用できsuccess
ます。したがって、この
var result = foo();
// Code that depends on 'result'
なる
foo(function(result) {
// Code that depends on 'result'
});
ここでは関数「インライン」を定義しましたが、任意の関数参照を渡すことができます。
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
foo
それ自体は次のように定義されます。
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
foo
呼び出すときに渡す関数を参照し、単にそれをに渡しsuccess
ます。つまり、Ajaxリクエストが成功$.ajax
するとcallback
、応答を呼び出してコールバックに渡します(これresult
はコールバックの定義方法であるため、で参照できます)。
コールバックに渡す前に応答を処理することもできます。
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
見かけよりもコールバックを使用してコードを記述する方が簡単です。結局のところ、ブラウザーのJavaScriptは非常にイベント駆動型(DOMイベント)です。Ajax応答の受信は、イベントに他なりません。
サードパーティのコードを使用する必要がある場合に問題が発生する可能性がありますが、ほとんどの問題は、アプリケーションフローを検討するだけで解決できます。
約束APIは、 ECMAScriptの6(ES2015)の新機能ですが、それは良い持っているブラウザのサポートをすでに。標準のPromises APIを実装し、非同期関数の使用と構成を容易にするための追加のメソッドを提供する多くのライブラリもあります(例:bluebird)。
約束は将来の価値のためのコンテナです。プロミスが値を受け取る(解決される)か、キャンセルされる(拒否される)と、この値にアクセスするすべての「リスナー」に通知されます。
プレーンコールバックに対する利点は、コードを分離できることと、簡単に作成できることです。
ここにプロミスを使用する簡単な例があります:
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
delay()
.then(function(v) { // `delay` returns a promise
console.log(v); // Log the value once it is resolved
})
.catch(function(v) {
// Or do something else if it is rejected
// (it would not happen in this example, since `reject` is not called).
});
Ajax呼び出しに適用すると、次のようなpromiseを使用できます。
function ajax(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve(this.responseText);
};
xhr.onerror = reject;
xhr.open('GET', url);
xhr.send();
});
}
ajax("/echo/json")
.then(function(result) {
// Code depending on result
})
.catch(function() {
// An error occurred
});
promiseが提供するすべての利点を説明することは、この回答の範囲を超えていますが、新しいコードを作成する場合は、真剣に検討する必要があります。それらはあなたのコードの素晴らしい抽象化と分離を提供します。
プロミスに関する詳細情報:HTML5のロック-JavaScriptプロミス
補足:jQueryの遅延オブジェクト
遅延オブジェクトは、jQueryのプロミスのカスタム実装です(Promise APIが標準化される前)。これらはpromiseとほとんど同じように動作しますが、多少異なるAPIを公開します。
jQueryのすべてのAjaxメソッドは、すでに「据え置きオブジェクト」(実際には据え置きオブジェクトの約束)を返します。これは、関数から返すことができます。
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
補足:約束の問題
promiseとdeferredオブジェクトは単なる将来の値のコンテナーであり、値そのものではないことに注意してください。たとえば、次のような場合を考えます。
function checkPassword() {
return $.ajax({
url: '/password',
data: {
username: $('#username').val(),
password: $('#password').val()
},
type: 'POST',
dataType: 'json'
});
}
if (checkPassword()) {
// Tell the user they're logged in
}
このコードは、上記の非同期の問題を誤解しています。具体的に$.ajax()
は、サーバーの「/ password」ページをチェックする間、コードをフリーズしません。サーバーにリクエストを送信し、待機している間、サーバーからの応答ではなく、jQuery Ajax Deferredオブジェクトをすぐに返します。つまり、if
ステートメントは常にこのDeferredオブジェクトを取得し、それをとして扱いtrue
、ユーザーがログインしているかのように処理します。
しかし、修正は簡単です:
checkPassword()
.done(function(r) {
if (r) {
// Tell the user they're logged in
} else {
// Tell the user their password was bad
}
})
.fail(function(x) {
// Tell the user something bad happened
});
非推奨:同期「Ajax」呼び出し
すでに述べたように、非同期操作の中には対応する同期操作があるものもあります(!)。私はそれらの使用を推奨しませんが、完全を期すために、同期呼び出しを実行する方法を次に示します。
jQueryなし
XMLHTTPRequest
オブジェクトを直接使用する場合は、false
3番目の引数としてに渡します.open
。
jQuery
jQueryを使用する場合は、async
オプションをに設定できますfalse
。このオプションはjQuery 1.8以降廃止されていることに注意してください。その後、success
コールバックを使用するかresponseText
、jqXHRオブジェクトのプロパティにアクセスできます。
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
あなたのような、他のjQueryのAjaxメソッドを使用している場合$.get
、$.getJSON
など、あなたはそれを変更する必要が$.ajax
(あなただけに設定パラメータを渡すことができるため$.ajax
)。
注意喚起!同期JSONPリクエストを行うことはできません。JSONPはその性質上、常に非同期です(このオプションを考慮しない理由の1つです)。