web-dev-qa-db-ja.com

特にループでのnode.jsを使用したJavaScriptコールバックの概念を理解する

Node.jsから始めたところです。私は少しajaxのことをしましたが、それほど複雑なことは何もないので、コールバックはまだ私の頭の上にあるようなものです。非同期を見ましたが、必要なのはいくつかの関数を順番に実行することだけです。

基本的に、APIからJSONを取得し、新しいJSONを作成して、それを使用して何かを実行するものがあります。明らかに、すべてを一度に実行し、JSONが空であるため、実行することはできません。ほとんどの場合、プロセスは順番に実行する必要がありますが、APIからJSONをプルしているときに、待機中に他のJSONをプルできる場合は、それで問題ありません。コールバックをループに入れるときに混乱しました。インデックスをどうすればよいですか?ループ内でコールバックを一種の再帰関数として使用し、forループをまったく使用しない場所をいくつか見たことがあると思います。

簡単な例が大いに役立ちます。

31
Mr JSON

コールバックがループが定義されているのと同じスコープで定義されている場合(これはよくあることです)、コールバックはインデックス変数にアクセスできます。 NodeJSの詳細はさておき、この関数について考えてみましょう。

function doSomething(callback) {
    callback();
}

その関数はコールバック関数参照を受け入れ、それを呼び出すだけです。あまりエキサイティングではありません。 :-)

それをループで使用しましょう:

var index;

for (index = 0; index < 3; ++index) {
    doSomething(function() {
        console.log("index = " + index);
    });
}

(サーバープロセスのような計算集約型のコードでは、本番コードで上記を文字通り実行しないのが最善です。すぐに戻ってきます。)

これを実行すると、期待される出力が表示されます。

index = 0
index = 1
index = 2

コールバックは、定義されているスコープ内のデータに対するクロージャであるため、コールバックはindexにアクセスできました。 (「クロージャ」という用語については心配しないでください クロージャは複雑ではありません 。)

計算集約型のプロダクションコードで上記を文字通り実行しないことがおそらく最善であると私が言った理由は、コードがすべての反復で関数を作成するためです(コンパイラでの派手な最適化を除いて、 V8は非常に賢いですが、これらの関数の作成を最適化することは簡単ではありません)。少し作り直した例を次に示します。

var index;

for (index = 0; index < 3; ++index) {
    doSomething(doSomethingCallback);
}

function doSomethingCallback() {
    console.log("index = " + index);
}

これは少し意外に見えるかもしれませんが、doSomethingCallbackはまだindexのクロージャであるため、同じように機能し、同じ出力を持ちます。したがって、呼び出された時点でindexの値が表示されます。しかし、今では、すべてのループに新しい関数ではなく、doSomethingCallback関数が1つだけあります。

ここで、が機能しないという否定的な例を見てみましょう。

foo();

function foo() {
    var index;

    for (index = 0; index < 3; ++index) {
        doSomething(myCallback);
    }
}

function myCallback() {
    console.log("index = " + index); // <== Error
}

myCallbackindexが定義されているのと同じスコープ(またはネストされたスコープ)で定義されていないため、indexmyCallback内で定義されていないため、失敗します。

最後に、ループ内にイベントハンドラーを設定することを検討しましょう。これには、注意が必要です。ここでは、NodeJSについて少し詳しく説明します。

var spawn = require('child_process').spawn;

var commands = [
    {cmd: 'ls', args: ['-lh', '/etc' ]},
    {cmd: 'ls', args: ['-lh', '/usr' ]},
    {cmd: 'ls', args: ['-lh', '/home']}
];
var index, command, child;

for (index = 0; index < commands.length; ++index) {
    command = commands[index];
    child = spawn(command.cmd, command.args);
    child.on('exit', function() {
        console.log("Process index " + index + " exited"); // <== WRONG
    });
}

上記のようには、以前のループと同じように機能するはずですが、決定的な違いがあります。以前のループでは、コールバックがすぐに呼び出されていたため、indexにはまだ先に進む機会がなかったため、正しいindex値が表示されました。ただし、上記では、コールバックが呼び出される前にループをスピンします。結果?私たちは見る

Process index 3 exited
Process index 3 exited
Process index 3 exited

これは重要なポイントです。クロージャには、クロージャするデータのコピーがなく、ライブ参照があります。したがって、これらの各プロセスでexitコールバックが実行されるまでに、ループはすでに完了しているため、3つの呼び出しすべてに同じindex値(endの時点での値)が表示されます。ループの)。

次のように、コールバックで変更されないdifferent変数を使用することで、これを修正できます。

var spawn = require('child_process').spawn;

var commands = [
    {cmd: 'ls', args: ['-lh', '/etc' ]},
    {cmd: 'ls', args: ['-lh', '/usr' ]},
    {cmd: 'ls', args: ['-lh', '/home']}
];
var index, command, child;

for (index = 0; index < commands.length; ++index) {
    command = commands[index];
    child = spawn(command.cmd, command.args);
    child.on('exit', makeExitCallback(index));
}

function makeExitCallback(i) {
    return function() {
        console.log("Process index " + i + " exited");
    };
}

ここで、正しい値を出力します(プロセスが終了する順序に関係なく)。

Process index 1 exited
Process index 2 exited
Process index 0 exited

動作する方法は、exitイベントに割り当てるコールバックが、iへの呼び出しでmakeExitCallback引数を閉じることです。 makeExitCallbackが作成して返す最初のコールバックは、iへのその呼び出しのmakeExitCallback値を閉じ、2番目のコールバックは、iへのthat呼び出しのmakeExitCallback値を閉じます(これは異なります)以前の呼び出しのi値よりも)など。

上記のリンク先の記事 を読むと、多くのことがより明確になるはずです。この記事の用語は少し古くなっていますが(ECMAScript 5は更新された用語を使用しています)、概念は変更されていません。

86
T.J. Crowder