node.jsが「循環+非同期」に遭遇した時の注意事項

5380 ワード

nodejsの特徴
nodejsの最大の特徴はすべて事件に基づくので、それによってすべて非同期です.nodejsの速度はなぜ速いですか?その原理はnginxと同じです.彼らはイベントのコールバックによって要求を処理しています.それによって全体の処理過程において、nodejsをブロックすることはありません.ですから、同じ時間で大量の要求を処理できます.この優越性はあなたの要求がIO密集型の場合、特に顕著です.以下の例は、非同期イベントに基づくnodejsの処理フローを簡単に説明する.
var send_data = function(req,res){
    sql = 'SELECT gid,name,image_url,price,create_time,describes,selluid FROM goods WHERE status=? LIMIT ?,?';
    connection.query(sql, [0,0,6], function(err, rows, fields) {
        if (err) throw err;
        console.log("  :             ");
    }); 
    console.log("  :           ");
};
nodejsを使ったプログラマーは分かりやすいはずです.この関数の出力結果は:
  :           
  :             
理由は簡単です.上のクエリー文はすぐに実行されるのではなく、実行待ちの列に入れてすぐに戻り、後のステートメントを実行し続けます.データベース操作が終了すると、あるイベントをトリガして、nodejsデータベース操作が完了したと伝えます.そこで、nodejsは設定されたコールバック関数を実行して、データベースの実行結果を処理します.これはまさにnodejsの高い効率の地方で、しかし、万事はいつも両面性があって、nodejsは高い効率の同時に、プログラマのプログラミングの複雑さをも増加して、非歩のプログラムと以前の同期のプログラムがとても大きい違いがあるため、次に私達は1つのよくある注意事項を見にきます.
forサイクル+非同期操作
経典的な問題はサイクルの中でコールバック関数に出会うことです.
var fs = require('fs');
var files = ['a.txt','b.txt','c.txt'];

for(var i=0; i < files.length; i++) {
    fs.readFile(files[i], 'utf-8', function(err, contents) {
        console.log(files[i] + ': ' + contents);
    });
}
この3つのファイルの内容はそれぞれAAA、BBB、CCCであると仮定します.
a.txt: AAA
b.txt: BBB
c.txt: CCC
実際の結果は、
undefined: AAA
undefined: BBB
undefined: CCC
これはなぜですか?循環内部でiの値をプリントアウトすれば、3回の出力のデータは全部3、つまりfiles.lengthの値です.つまり、fs.readFileのコールバック関数で訪問したi値は全部サイクル終了後の値なので、files[i]の値はundefinedとなります.この問題を解決するためには多くの方法があります.ここではjs関数のプログラミングの特性を利用して、必要な毎にi値を保存するためにクローズドを作ります.
var fs = require('fs');
var files = ['a.txt','b.txt','c.txt'];

for(var i=0; i < files.length; i++) {
    (function(i) {
        fs.readFile(files[i], 'utf-8', function(err, contents) {
            console.log(files[i] + ': ' + contents);
        });
    })(i);
}
実行時のクローズド・パケットの存在により、匿名関数で定義された変数(パラメータテーブルを含む)は、その内部の関数(fs.readFileのコールバック関数)が実行される前に解放されないので、訪問したiはそれぞれ異なるクローズド・インスタンスであり、この実施例は、循環体実行中に作成され、異なる値を保持している.ここでクローズドを使うのは上からundefinedが出力されている理由をより明確に見るためです.実は、もっと簡単な方法があります.
var fs = require('fs');
var files = ['a.txt', 'b.txt', 'c.txt'];

files.forEach(function(filename) {
    fs.readFile(filename, 'utf-8', function(err, contents) {
        console.log(filename + ': ' + contents);
    });
});
関連する複数のsqlクエリ操作があります.
上のforサイクルからは、非同期プログラミングと同期プログラミングの違いがはっきり見えます.高効率ですが、ピットが多いです.例えば、もし二回のsql操作が必要だったら、明確な必要があります.二回目は一回目の完成後に行わなければならないです.どうすればいいですか?これは簡単です.一回目のコールバック関数の内部に2回目の操作を書くだけでいいです.一回目のコールバック関数がトリガする前提はすでに実行済みです.しかし、2回目の操作では、1回目の操作で返ってきたデータがクエリー条件として必要であり、2回目の結果を統合して返すには、どうすればいいですか?次のようですか?
var send_data = function(req,res){
    sql = 'SELECT gid,name,image_url,price,create_time,describes,selluid FROM goods WHERE status=? LIMIT ?,?';
    connection.query(sql, [0,0,6], function(err, rows, fields) {
        if (err) throw err;
        rows.forEach(function(item){
            sql = "SELECT tag_name FROM tag,tag_goods WHERE tag_goods.gid=? AND tag_goods.tagid=tag.tagid";
            connection.query(sql, item.gid, function(err, tags, fields){
                if (err) throw err;
                item.tags = tags;
            });
        }); 
        res.render('index', {supplies:rows, login:req.session.login}); 
    }
};
上記の例は、まず商品の情報を調べて、それぞれの商品に対して、タグリストをそのIDで調べて、商品ごとの情報に追加します.上に戻った結果は本当に期待通りですか?しかし、ラベルが含まれていない商品情報だけが返ってきた.つまり、内層クエリの実行が終了するまでは、res.render()方法は戻ってきた.第二条クエリーは第一条クエリーが終わってから実行することを保証しましたが、戻り文は第二条クエリーが終わってから戻るという保証はありません.具体的な解決方法はいろいろありますが、ここではasyncモジュールを使ってここの同期問題を解決します.
ASync関数紹介
asyncは主に多くの有用な関数を実現しました.
  • each:同じセットのすべての要素に対して同じ非同期動作を実行したいなら.
  • map:集合中の各要素に対して、ある非同期動作を実行し、結果を得る.すべての結果は最終的なcalbackにまとめられます.eachとの違いは、eachが最後の値に関わらず、mapが関心を持つ最後に発生する値だけに関心があります.
  • series:シリアルで実行します.各関数が実行された後、次の関数が実行されます.
  • parallel:複数の関数を並列に実行し、各関数は直ちに実行され、他の関数が先に実行されるまで待つ必要はない.最終的なcalbackに送られる配列のデータは、完了した順序ではなくtaskで宣言された順序に従います.
  • 他の
  • 明らかにここではmap関数を使って私たちの需要を実現できます.この方法の原型はmap(arr,iterator),calback(err,reults)である.つまり、私たちはarrの各要素itemで反復的にiteratorを呼び出して、その都度の結果を保存します.反復が終わったら、結果をまとめてresultsにcalbackを呼び出す方法です.この方法を適用して、私達のプログラムは次のように変更されました.
    var async = require('async');
    
    var send_data = function(req,res){
        sql = 'SELECT gid,name,image_url,price,create_time,describes,selluid FROM goods WHERE status=? LIMIT ?,?';
        connection.query(sql, [0,0,6], function(err, rows, fields) {
            if (err) throw err;
            async.map(rows, function(item, callback) {
                sql = "SELECT tag_name FROM tag,tag_goods WHERE tag_goods.gid=? AND tag_goods.tagid=tag.tagid";
                connection.query(sql, item.gid, function(err, tags, fields){
                    item.tags = tags;
                    callback(null, item);
                });
            }, function(err,results) {
                res.render('index', {supplies:results, login:req.session.login});
            });
        }); 
    };
    
    この時、第二のsql文は毎回検索した「itemに保存され、すべての検索が終わったら、calback(null、item)を呼び出します.すべてのデータをレスポンスパラメータに転送し、ブラウザに一括送信します.この時送った商品には、商品ラベルが含まれています.