Node.jsの非同期プログラミングの中のコールバックとコードの設計モードを分析します.

7274 ワード

NodeJSの最大の売りは、イベントメカニズムと非同期IOであり、開発者には透明ではない.開発者は非同期的にコードを作成しなければならないので、このセールスポイントは使えません.この点はNodeJSの反対者からも非難されました.いずれにしても、非同期プログラムは確実にNodeJSの最大の特徴です.非同期プログラムを把握していないと、NodeJSを本当に習得したとは言えません.この章では、非同期プログラミングに関するさまざまな知識を紹介します.
コードの中で、非同期プログラミングの直接的な表現は逆変調です.非同期プログラミングはコールバックによって実現されますが、コールバックを使ってプログラムが非同期化されたとは言えません.まず以下のコードを見てもいいです.

function heavyCompute(n, callback) {
 var count = 0,
  i, j;

 for (i = n; i > 0; --i) {
  for (j = n; j > 0; --j) {
   count += 1;
  }
 }

 callback(count);
}

heavyCompute(10000, function (count) {
 console.log(count);
});

console.log('hello');


100000000
hello
上記のコードの中のコールバック関数は、後続のコードよりも先に実行されていることがわかる.JS自体はシングルスレッドで実行されており、コードの一部がまだ終わっていないときに別のコードを実行することは不可能であるため、非同期実行の概念は存在しない.
しかし、ある関数が行うことが個々のスレッドやプロセスを作成し、JSのメインスレッドと並行して行うものであれば、仕事が終わったらJSのメインスレッドに通知するということは、また違っています.私たちは次のコードを見ます.

setTimeout(function () {
 console.log('world');
}, 1000);

console.log('hello');


hello
world
今回は、コールバック関数の後に、後続のコードで実行されることが分かります.上記のように、JS自体はシングルスレッドであり、非同期では実行できないので、setTimeoutのようなJS仕様以外の運転環境によって提供される特殊な関数で行うことは、平行スレッドを作成してからすぐに戻り、JSメインプロセスが後続コードを実行し、平行プロセスの通知を受けてから再度コールバック関数を実行することができると考えられます.setTimeout、set Intervalなどの一般的な関数に加えて、NodeJSが提供するfs.readFileなどの非同期APIも含む.
また、JSに戻ってもシングルスレッドで実行されているという事実は、JSがコードの一部を実行する前に、コールバック関数を含む他のコードを実行できないことを決定しました.つまり、平行スレッドが動作していても、JSメインスレッドにレス関数を実行するよう通知します.コールバック関数はJSメインラインが空いてから実行します.以下はこのような例です.

function heavyCompute(n) {
 var count = 0,
  i, j;

 for (i = n; i > 0; --i) {
  for (j = n; j > 0; --j) {
   count += 1;
  }
 }
}

var t = new Date();

setTimeout(function () {
 console.log(new Date() - t);
}, 1000);

heavyCompute(50000);


8520
JSメインラインは他のコードを実行するのに忙しいので、1秒後に呼び出されるはずのコールバック関数が、実際の実行時間を大幅に遅らせていることが分かります.
コード設計モードの非同期プログラミングには多くの特有なコード設計モードがあります.同じ機能を実現するために、同期方式と非同期方式を使って作成したコードは大きな違いがあります.一般的なパターンを紹介します.
関数の戻り値は、関数の出力を別の関数として使う入力が一般的です.同期方式では、一般的に以下のようにコードを作成します.

var output = fn1(fn2('input'));
// Do something.
非同期方式では、関数の実行結果は戻り値ではなく、コールバック関数によって伝達されるので、コードは一般的に以下のように作成される.

fn2('input', function (output2) {
 fn1(output2, function (output1) {
  // Do something.
 });
});
このような方法は一つのコールバック関数がセットになっています.一つのコールバックが多くて、セットが多すぎて、簡単に書き出すことができます.
エルゴード配列は配列を巡回する時、ある関数を使ってデータのメンバーに順番にいくつかの処理をするのもよくある需要です.関数が同期して実行される場合、通常は以下のコードが書かれます.

var len = arr.length,
 i = 0;

for (; i < len; ++i) {
 arr[i] = sync(arr[i]);
}

// All array items have processed.

関数が非同期で実行されている場合、上記のコードはループ終了後の配列全員の処理が完了したとは保証できません.配列メンバーが連続して処理しなければならない場合、通常は以下のように非同期コードを作成する.

(function next(i, len, callback) {
 if (i < len) {
  async(arr[i], function (value) {
   arr[i] = value;
   next(i + 1, len, callback);
  });
 } else {
  callback();
 }
}(0, arr.length, function () {
 // All array items have processed.
}));
上記のコードは非同期関数で一回実行され、実行結果に戻ってから次の配列メンバーに伝えられ、次のラウンドの実行が開始されるまで、全ての配列メンバーが処理が終了した後、チューニングによって後続コードの実行がトリガされることが見られます.
配列メンバーが並列に処理できますが、後続コードはまだ全ての配列メンバーが処理済みで実行される必要がある場合、非同期コードは以下のように調整されます.

(function (i, len, count, callback) {
 for (; i < len; ++i) {
  (function (i) {
   async(arr[i], function (value) {
    arr[i] = value;
    if (++count === len) {
     callback();
    }
   });
  }(i));
 }
}(0, arr.length, 0, function () {
 // All array items have processed.
}));
上記のコードは非同期のシリアルエルゴードバージョンと比較して、すべての配列メンバーを並列処理し、カウンタ変数によっていつすべての配列メンバーが処理済みかを判断することができます.
異常処理JS自身が提供する異常捕獲と処理の仕組み――try.ccach.は、同期実行のためのコードのみに使用されます.以下は一例です.

function sync(fn) {
 return fn();
}

try {
 sync(null);
 // Do something.
} catch (err) {
 console.log('Error: %s', err.message);
}


Error: object is not a function
異常はコードの実行経路に沿って泡が発生し、最初のtry文に出会うまで捕獲されることがわかる.しかし、非同期関数がコードの実行経路を中断し、非同期関数が実行中及び実行後に発生した異常な泡が実行経路が中断された位置に来たとき、try文に遭遇していなかった場合、グローバル異常として投げ出します.以下は一例です.

function async(fn, callback) {
 // Code execution path breaks here.
 setTimeout(function () {
  callback(fn());
 }, 0);
}

try {
 async(null, function (data) {
  // Do something.
 });
} catch (err) {
 console.log('Error: %s', err.message);
}


/home/user/test.js:4
  callback(fn());
     ^
TypeError: object is not a function
 at null._onTimeout (/home/user/test.js:4:13)
 at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)
コードの実行経路が中断されたので、異常な泡が断線に達する前にtry文で異常を捕捉し、捕獲された異常をフィードバック関数で伝える必要があります.そこで私たちは下のように上の例を改造することができます.

function async(fn, callback) {
 // Code execution path breaks here.
 setTimeout(function () {
  try {
   callback(null, fn());
  } catch (err) {
   callback(err);
  }
 }, 0);
}

async(null, function (err, data) {
 if (err) {
  console.log('Error: %s', err.message);
 } else {
  // Do something.
 }
});


Error: object is not a function
異常が再び捕獲されたことが見られます.NodeJSでは、ほぼすべての非同期APIが上記のように設計されており、コールバック関数の最初のパラメータはerrである.したがって、私たちは自分の非同期関数を作成する時に、このように異常を処理してもいいです.NodeJSの設計スタイルと一致しています.
異常処理方式があったら、普通コードはどう書きますか?基本的には、コードはいくつかのことをして、関数を呼び出して、いくつかのことをして、関数を呼び出します.同期コードを書いたら、コードの入り口にtry文を書くだけで、すべての泡が発生する異常を捕捉できます.例は以下の通りです.

function main() {
 // Do something.
 syncA();
 // Do something.
 syncB();
 // Do something.
 syncC();
}

try {
 main();
} catch (err) {
 // Deal with exception.
}

しかし、もし私たちが非同期コードを書いたら、ほほほしかないです.非同期関数が呼び出されるたびにコードの実行経路を中断し、コールバック関数によってしか異常を伝達できないので、各コールバック関数で異常が発生しているかどうかを判断する必要があります.そこで、非同期関数を3回だけ呼び出したら、次のようなコードが発生します.

function main(callback) {
 // Do something.
 asyncA(function (err, data) {
  if (err) {
   callback(err);
  } else {
   // Do something
   asyncB(function (err, data) {
    if (err) {
     callback(err);
    } else {
     // Do something
     asyncC(function (err, data) {
      if (err) {
       callback(err);
      } else {
       // Do something
       callback(null);
      }
     });
    }
   });
  }
 });
}

main(function (err) {
 if (err) {
  // Deal with exception.
 }
});

コールバック関数はコードを複雑にしていますが、非同期方式では異常な処理がコードの複雑さを増しています.