関数内で変数を変更した後、変数が変更されないのはなぜですか?-非同期コードリファレンス
次の例を考えると、なぜouterScopeVar
すべての場合に未定義なのですか?
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = 'lolcat.png';
alert(outerScopeVar);
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello Asynchronous World!';
}, 0);
alert(outerScopeVar);
// Example using some jQuery
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
});
alert(outerScopeVar);
// Node.js example
var outerScopeVar;
fs.readFile('./catdog.html', function(err, data) {
outerScopeVar = data;
});
console.log(outerScopeVar);
// with promises
var outerScopeVar;
myPromise.then(function (response) {
outerScopeVar = response;
});
console.log(outerScopeVar);
// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
outerScopeVar = pos;
});
console.log(outerScopeVar);
undefined
これらすべての例で出力されるのはなぜですか?回避策は必要ありません。なぜこれが発生しているのかを知りたいのです。
注:これは、JavaScriptの非同期性に関する標準的な質問です。この質問を自由に改善し、コミュニティが識別できるより単純化された例を追加してください。
回答
一言で言えば、非同期性。
はじめに
このトピックは、ここではStackOverflowで少なくとも数千回繰り返されています。したがって、最初にいくつかの非常に有用なリソースを指摘したいと思います。
@Felix Klingの「非同期呼び出しから応答を返すにはどうすればよいですか?」に対する回答 。同期フローと非同期フローを説明する彼の優れた回答と、「コードの再構築」セクションを参照してください。
@Benjamin Gruenbaumも、同じスレッドで非同期性を説明するために多大な努力を払っています。「fs.readFileからデータを取得する」に対する@MattEschの回答も、非同期性を簡単な方法で非常によく説明しています。
手元の質問への答え
まず、一般的な動作を追跡しましょう。すべての例で、outerScopeVar
は関数内で変更されます。その関数は明らかにすぐには実行されず、引数として割り当てられるか渡されます。それがコールバックと呼ばれるものです。
ここで問題は、そのコールバックはいつ呼び出されるのかということです。
場合によって異なります。いくつかの一般的な動作をもう一度トレースしてみましょう。
img.onload
将来、イメージが正常にロードされたとき(およびその場合)に呼び出される可能性があります。setTimeout
遅延が期限切れになり、タイムアウトがによってキャンセルされなかった後、将来いつか呼び出される可能性がありますclearTimeout
。注:0
遅延として使用する場合でも、すべてのブラウザーには最小タイムアウト遅延上限があります(HTML5仕様では4msと指定されています)。- jQuery
$.post
のコールバックは、将来、Ajaxリクエストが正常に完了したときに呼び出される可能性があります。 - Node.jsは、ファイルが正常に読み取られたとき、またはエラーがスローされたときに、将来
fs.readFile
呼び出される可能性があります。
すべての場合において、将来いつか実行される可能性のあるコールバックがあります。この「将来のいつか」は、非同期フローと呼ばれるものです。
非同期実行は同期フローからプッシュされます。つまり、同期コードスタックの実行中は、非同期コードは実行されません。これは、JavaScriptがシングルスレッドであることの意味です。
より具体的には、JSエンジンがアイドル状態の場合((a)同期コードのスタックを実行していない場合)、非同期コールバックをトリガーした可能性のあるイベント(タイムアウトの期限切れ、ネットワーク応答の受信など)をポーリングし、それらを次々に実行します。これはイベントループと見なされます。
つまり、手描きの赤い形で強調表示されている非同期コードは、それぞれのコードブロックに残っているすべての同期コードが実行された後にのみ実行できます。
つまり、コールバック関数は同期的に作成されますが、非同期的に実行されます。非同期関数が実行されたことを知るまで、非同期関数の実行に頼ることはできません。その方法は?
本当に簡単です。非同期関数の実行に依存するロジックは、この非同期関数の内部から開始/呼び出す必要があります。たとえば、alert
sとconsole.log
sをコールバック関数内に移動すると、結果がその時点で利用可能になるため、期待される結果が出力されます。
独自のコールバックロジックの実装
多くの場合、非同期関数が呼び出された場所に応じて、非同期関数の結果を使用してさらに多くのことを実行したり、結果を使用して別のことを実行したりする必要があります。もう少し複雑な例に取り組みましょう。
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'Nya';
}, Math.random() * 2000);
}
注:私が使用しているsetTimeout
一般的な非同期関数としてランダム遅延と、同じ例では、アヤックスに適用されreadFile
、onload
および任意の他の非同期の流れ。
この例は明らかに他の例と同じ問題を抱えており、非同期関数が実行されるまで待機していません。
独自のコールバックシステムを実装して取り組みましょう。まずouterScopeVar
、この場合はまったく役に立たない醜いものを取り除きます。次に、関数の引数を受け入れるパラメーター、コールバックを追加します。非同期操作が終了すると、このコールバックを呼び出して結果を渡します。実装(順番にコメントを読んでください):
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
alert(result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
// 3. Start async operation:
setTimeout(function() {
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
上記の例のコードスニペット:
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
console.log("1. function called...")
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
console.log("5. result is: ", result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
console.log("2. callback here is the function passed as argument above...")
// 3. Start async operation:
setTimeout(function() {
console.log("3. start async operation...")
console.log("4. finished async operation, calling the callback, passing the result...")
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
ほとんどの場合、実際のユースケースでは、DOM APIとほとんどのライブラリがすでにコールバック機能を提供しています(helloCatAsync
この実例の実装)。コールバック関数を渡して、それが同期フローから実行されることを理解し、それに対応するようにコードを再構築するだけです。
また、非同期の性質上、return
非同期コールバックは同期コードの実行が終了してからかなり経ってから実行されるため、非同期フローからコールバックが定義された同期フローに値を戻すことはできません。
return
非同期コールバックから値を取得する代わりに、コールバックパターンを使用する必要があります。または... Promises。
約束
バニラJSでコールバックの地獄を寄せ付けない方法はありますが、Promiseの人気が高まっており、現在ES6で標準化されています(Promise-MDNを参照)。
Promises(別名Futures)は、非同期コードのより直線的な、したがって快適な読み取りを提供しますが、それらの機能全体を説明することは、この質問の範囲外です。代わりに、興味のある人のためにこれらの優れたリソースを残しておきます。
JavaScriptの非同期性に関するその他の資料
- Art of Node-Callbacksは、非同期コードとコールバックを、バニラJSの例とNode.jsコードで非常によく説明しています。
注:この回答をコミュニティWikiとしてマークしたので、評判が100以上の人なら誰でも編集して改善できます。この回答を自由に改善するか、必要に応じてまったく新しい回答を送信してください。
この質問を標準的なトピックに変えて、Ajaxとは関係のない非同期性の問題に答えたいと思います(そのためにAJAX呼び出しから応答を返す方法がありますか?)、したがって、このトピックはできるだけ良くて役立つようにあなたの助けが必要です!!
ファブリシオの答えは的を射ています。しかし、私は彼の答えを、非同期性の概念を説明するのに役立つアナロジーに焦点を当てた、あまり技術的ではないもので補完したかったのです。
アナロジー...
昨日、私が行っていた作業には、同僚からの情報が必要でした。私は彼に電話をかけた。会話の流れは次のとおりです。
私:こんにちはボブ、私は先週私たちがどのようにバーを見つけたのかを知る必要があります。ジムはそれについての報告を望んでいます、そしてあなたはそれについての詳細を知っている唯一の人です。
ボブ:もちろんですが、30分ほどかかりますか?
私:それは素晴らしいボブです。あなたが情報を手に入れたら、私にリングを返してください!
この時点で、私は電話を切りました。レポートを完成させるためにボブからの情報が必要だったので、レポートを残して代わりにコーヒーを飲みに行きました。その後、メールに追いつきました。40分後(ボブは遅い)、ボブは電話をかけ直し、必要な情報を教えてくれました。この時点で、必要な情報がすべて揃ったので、レポートを使用して作業を再開しました。
代わりに、会話がこのようになったと想像してみてください。
私:こんにちはボブ、私は先週私たちがどのようにバーを見つけたのかを知る必要があります。ジムはそれについてのレポートを望んでいます、そしてあなたはそれについての詳細を知っている唯一の人です。
ボブ:もちろんですが、30分ほどかかりますか?
私:それは素晴らしいボブです。待ちます。
そして私はそこに座って待った。そして待った。そして待った。40分間。待つ以外に何もしません。最終的に、ボブは私に情報を提供し、私たちは電話を切り、私はレポートを完成させました。しかし、40分の生産性が失われました。
これは非同期動作と同期動作です
これはまさに私たちの質問のすべての例で起こっていることです。画像の読み込み、ディスクからのファイルの読み込み、AJAXを介したページのリクエストはすべて、(最新のコンピューティングのコンテキストでは)遅い操作です。
JavaScriptでは、これらの低速操作が完了するのを待つのではなく、低速操作が完了したときに実行されるコールバック関数を登録できます。ただし、その間、JavaScriptは他のコードを実行し続けます。JavaScriptが遅い操作が完了するのを待っている間に他のコードを実行するという事実は、動作を非同期にします。JavaScriptが他のコードを実行する前に操作が完了するのを待っていたとしたら、これは同期動作でした。
var outerScopeVar;
var img = document.createElement('img');
// Here we register the callback function.
img.onload = function() {
// Code within this function will be executed once the image has loaded.
outerScopeVar = this.width;
};
// But, while the image is loading, JavaScript continues executing, and
// processes the following lines of JavaScript.
img.src = 'lolcat.png';
alert(outerScopeVar);
上記のコードlolcat.png
では、JavaScriptにロードを要求しています。これは遅い操作です。この遅い操作が完了すると、コールバック関数が実行されますが、その間、JavaScriptは次のコード行を処理し続けます。すなわちalert(outerScopeVar)
。
これが、アラートが表示される理由undefined
です。以来、alert()
すぐに処理されるのではなく、イメージがロードされた後。
コードを修正するには、alert(outerScopeVar)
コードをコールバック関数に移動するだけです。この結果outerScopeVar
、グローバル変数として宣言された変数は不要になりました。
var img = document.createElement('img');
img.onload = function() {
var localScopeVar = this.width;
alert(localScopeVar);
};
img.src = 'lolcat.png';
JavaScriptでコードを定義する唯一の方法*であるため、コールバックが関数として指定されていることが常にわかりますが、それは後で実行されるまで実行されません。
したがって、すべての例で、function() { /* Do something */ }
はコールバックです。すべての例を修正するには、操作の応答が必要なコードをそこに移動するだけです。
*は、技術的にあなたが使用することができるeval()
だけでなく、しかし、eval()
悪である、この目的のために
発信者を待たせるにはどうすればよいですか?
現在、これに似たコードがあるかもしれません。
function getWidthOfImage(src) {
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = src;
return outerScopeVar;
}
var width = getWidthOfImage('lolcat.png');
alert(width);
しかし、今ではreturn outerScopeVar
すぐに起こることがわかりました。onload
コールバック関数が変数を更新する前。これにより、がgetWidthOfImage()
返されundefined
、undefined
警告が表示されます。
これを修正するには、関数呼び出しgetWidthOfImage()
がコールバックを登録できるようにしてから、幅のアラートをそのコールバック内に移動する必要があります。
function getWidthOfImage(src, cb) {
var img = document.createElement('img');
img.onload = function() {
cb(this.width);
};
img.src = src;
}
getWidthOfImage('lolcat.png', function (width) {
alert(width);
});
...前と同じように、グローバル変数(この場合width
)を削除できたことに注意してください。
クイックリファレンスを探している人のためのより簡潔な答えと、promiseとasync / awaitを使用したいくつかの例を次に示します。
非同期メソッド(この場合setTimeout
)を呼び出してメッセージを返す関数の単純なアプローチ(これは機能しません)から始めます。
function getMessage() {
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello asynchronous world!';
}, 0);
return outerScopeVar;
}
console.log(getMessage());
undefined
この場合getMessage
、setTimeout
コールバックが呼び出されて更新される前に戻るため、ログに記録されますouterScopeVar
。
それを解決する2つの主な方法は、コールバックとプロミスを使用することです。
コールバック
ここでの変更は、使用可能になったときに呼び出し元のコードに結果を返すために呼び出されるパラメーターをgetMessage
受け入れるcallback
ことです。
function getMessage(callback) {
setTimeout(function() {
callback('Hello asynchronous world!');
}, 0);
}
getMessage(function(message) {
console.log(message);
});
Promiseは、複数の非同期操作を調整するために自然に組み合わせることができるため、コールバックよりも柔軟な代替手段を提供します。約束/ A +標準実装では、ネイティブのNode.js(0.12+)と現在の多くのブラウザで提供されるが、同様ライブラリで実装されているブルーバードとQ。
function getMessage() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
getMessage().then(function(message) {
console.log(message);
});
jQueryの延期
jQueryは、Deferredsのpromiseと同様の機能を提供します。
function getMessage() {
var deferred = $.Deferred();
setTimeout(function() {
deferred.resolve('Hello asynchronous world!');
}, 0);
return deferred.promise();
}
getMessage().done(function(message) {
console.log(message);
});
非同期/待機
JavaScript環境にasync
andのサポートが含まれている場合await
(Node.js 7.6+など)、async
関数内でpromiseを同期的に使用できます。
function getMessage () {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
async function main() {
let message = await getMessage();
console.log(message);
}
main();
明白なことを言うと、カップはを表しouterScopeVar
ます。
非同期関数は次のようになります...
他の答えは素晴らしいです、そして私はこれに簡単な答えを提供したいと思います。jQuery非同期呼び出しに限定するだけ
すべてのajax呼び出し($.get
または$.post
またはを含む$.ajax
)は非同期です。
あなたの例を考える
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2
outerScopeVar = response;
});
alert(outerScopeVar); //line 3
コードの実行は1行目から始まり、変数を宣言し、2行目で非同期呼び出し(つまり、postリクエスト)をトリガーし、postリクエストの実行が完了するのを待たずに3行目から実行を継続します。
ポストリクエストが完了するまでに10秒かかるとしouterScopeVar
ましょう。の値は、その10秒後にのみ設定されます。
試してみるには、
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2, takes 10 seconds to complete
outerScopeVar = response;
});
alert("Lets wait for some time here! Waiting is fun"); //line 3
alert(outerScopeVar); //line 4
これを実行すると、3行目にアラートが表示されます。POSTリクエストが何らかの値を返したことを確認するまで、しばらく待ちます。次に、[OK]をクリックすると、アラートボックスで、待機していたため、次のアラートで期待値が出力されます。
実際のシナリオでは、コードは次のようになります。
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
alert(outerScopeVar);
});
非同期呼び出しに依存するすべてのコードは、非同期ブロック内に移動されるか、非同期呼び出しを待機します。
これらすべてのシナリオouterScopeVar
で、非同期で値が変更または割り当てられるか、後で発生し(イベントの発生を待機またはリッスン)、現在の実行は待機しません。したがって、これらすべての場合、現在の実行フローは次のようになります。outerScopeVar = undefined
それぞれの例について説明しましょう(非同期と呼ばれる部分、または一部のイベントが発生するために遅延する部分にマークを付けました)。
1.1。
ここでは、その特定のイベントで実行されるイベントリスナーを登録します。ここで画像をロードします。次に、現在の実行は次の行に続きimg.src = 'lolcat.png';
、alert(outerScopeVar);
その間、イベントは発生しない可能性があります。つまり、関数img.onload
は参照されたイメージが非同期にロードされるのを待ちます。これは、次のすべての例で発生します-イベントは異なる場合があります。
2.2。
ここでは、タイムアウトイベントが役割を果たし、指定された時間の後にハンドラーを呼び出します。ここにありますが0
、それでも非同期イベントを登録し、Event Queue
実行の最後の位置に追加されるため、遅延が保証されます。
3.3。
4.4。
ノードは非同期コーディングの王様と見なすことができます。ここで、マークされた関数は、指定されたファイルを読み取った後に実行されるコールバックハンドラーとして登録されます。
5.5。
明らかな約束(将来何かが行われる)は非同期です。JavaScriptのDeferred、Promise、Futureの違いは何ですか?を参照してください。
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript