web-dev-qa-db-ja.com

node.jsで非同期ウォーターフォールを使用する

非同期に実行している2つの関数があります。ウォーターフォールモデルを使用して記述したいと思います。事は、私は方法がわからないということです。

ここに私のコードがあります:

var fs = require('fs');
function updateJson(ticker, value) {
  //var stocksJson = JSON.parse(fs.readFileSync("stocktest.json"));
  fs.readFile('stocktest.json', function(error, file) {
    var stocksJson =  JSON.parse(file);

    if (stocksJson[ticker]!=null) {
      console.log(ticker+" price : " + stocksJson[ticker].price);
      console.log("changing the value...")
      stocksJson[ticker].price =  value;

      console.log("Price after the change has been made -- " + stocksJson[ticker].price);
      console.log("printing the the Json.stringify")
      console.log(JSON.stringify(stocksJson, null, 4));
      fs.writeFile('stocktest.json', JSON.stringify(stocksJson, null, 4), function(err) {  
        if(!err) {
          console.log("File successfully written");
        }
        if (err) {
          console.error(err);
        }
      }); //end of writeFile
    } else {
      console.log(ticker + " doesn't exist on the json");
    }
  });
} // end of updateJson 

ウォーターフォールを使用してどのように書くことができますか?これを制御できますか? node.jsが初めてなので、例をいくつか書いてください

41
user3502786

最初にステップを識別し、非同期関数として記述します(コールバック引数を取得します)

  • ファイルを読む

    function readFile(readFileCallback) {
        fs.readFile('stocktest.json', function (error, file) {
            if (error) {
                readFileCallback(error);
            } else {
                readFileCallback(null, file);
            }
        });
    }
    
  • ファイルを処理します(例のconsole.logのほとんどを削除しました)

    function processFile(file, processFileCallback) {
        var stocksJson = JSON.parse(file);
        if (stocksJson[ticker] != null) {
            stocksJson[ticker].price = value;
            fs.writeFile('stocktest.json', JSON.stringify(stocksJson, null, 4), function (error) {
                if (err) {
                    processFileCallback(error);
                } else {
                    console.log("File successfully written");
                    processFileCallback(null);
                }
            });
        }
        else {
            console.log(ticker + " doesn't exist on the json");
            processFileCallback(null); //callback should always be called once (and only one time)
        }
    }
    

ここでは特定のエラー処理を行っていないことに注意してください。async.waterfallを利用して、同じ場所でエラー処理を一元化します。

また、非同期関数に(if/else/switch/...)ブランチがある場合は、常に1回だけコールバックを呼び出すことに注意してください。

Async.waterfallですべてをプラグインする

async.waterfall([
    readFile,
    processFile
], function (error) {
    if (error) {
        //handle readFile error or processFile error here
    }
});

きれいな例

前のコードは、説明を明確にするために過度に冗長でした。完全にクリーンな例を次に示します。

async.waterfall([
    function readFile(readFileCallback) {
        fs.readFile('stocktest.json', readFileCallback);
    },
    function processFile(file, processFileCallback) {
        var stocksJson = JSON.parse(file);
        if (stocksJson[ticker] != null) {
            stocksJson[ticker].price = value;
            fs.writeFile('stocktest.json', JSON.stringify(stocksJson, null, 4), function (error) {
                if (!err) {
                    console.log("File successfully written");
                }
                processFileCallback(err);
            });
        }
        else {
            console.log(ticker + " doesn't exist on the json");
            processFileCallback(null);
        }
    }
], function (error) {
    if (error) {
        //handle readFile error or processFile error here
    }
});

関数名は読みやすくし、chromeデバッガーなどのツールでデバッグできるため、関数名を残しました。

アンダースコアnpm )を使用する場合、最初の関数を_.partial(fs.readFile, 'stocktest.json')に置き換えることもできます

59
Volune

何よりもまず、 async.waterfallに関するドキュメントをお読みください であることを確認してください。

さて、ウォーターフォール制御フローにはいくつかの重要な部分があります。

  1. 制御フローは、最初の引数として呼び出し用の関数の配列によって指定され、2番目の引数としてフローが終了したときに「完全な」コールバックによって指定されます。
  2. 関数の配列は、series(並列ではなく)で呼び出されます。
  3. エラー(通常errという名前)がフロー配列のいずれかの操作で発生した場合、それは短絡し、すぐに "complete"/"finish"/"done" callbackを呼び出します。
  4. 以前に実行された関数からの引数は、制御フロー内の次の関数に 適用 で、最後の引数として「中間」コールバックが提供されます。注:最初の関数にはこの「中間」コールバックのみがあり、「完了」コールバックには、制御フローで最後に呼び出された関数の引数が(エラーを考慮して)ありますが、「中間」の代わりにerr引数が付加されます「追加されるコールバック。
  5. 個々の操作のコールバック(この例ではこのcbAsyncを呼び出します)は、先に進む準備ができたら呼び出す必要があります。最初のパラメーターはエラーであり、2番目(3番目、4番目...など)になります。 )パラメータは、後続の操作に渡すデータです。

最初の目標は、async.waterfallの導入と並行して、コードをほぼそのまま動作させることです。 console.logステートメントをすべて削除し、エラー処理を簡素化することにしました。最初の反復(テストされていないコード):

var fs = require('fs'),
    async = require('async');

function updateJson(ticker,value) {
    async.waterfall([ // the series operation list of `async.waterfall`
        // waterfall operation 1, invoke cbAsync when done
        function getTicker(cbAsync) {
            fs.readFile('stocktest.json',function(err,file) {
                if ( err ) {
                    // if there was an error, let async know and bail
                    cbAsync(err);
                    return; // bail
                }
                var stocksJson = JSON.parse(file);
                if ( stocksJson[ticker] === null ) {
                    // if we don't have the ticker, let "complete" know and bail
                    cbAsync(new Error('Missing ticker property in JSON.'));
                    return; // bail
                }
                stocksJson[ticker] = value;
                // err = null (no error), jsonString = JSON.stringify(...)
                cbAsync(null,JSON.stringify(stocksJson,null,4));    
            });
        },
        function writeTicker(jsonString,cbAsync) {
            fs.writeFile('stocktest.json',jsonString,function(err) {
                cbAsync(err); // err will be null if the operation was successful
            });
        }
    ],function asyncComplete(err) { // the "complete" callback of `async.waterfall`
        if ( err ) { // there was an error with either `getTicker` or `writeTicker`
            console.warn('Error updating stock ticker JSON.',err);
        } else {
            console.info('Successfully completed operation.');
        }
    });
}

2番目の反復では、操作フローをもう少し分割します。それは、より小さな単一操作指向のコードの塊にそれを置きます。私はそれをコメントするつもりはありません、それはそれ自身のために話します(再び、テストされていない):

var fs = require('fs'),
    async = require('async');

function updateJson(ticker,value,callback) { // introduced a main callback
    var stockTestFile = 'stocktest.json';
    async.waterfall([
        function getTicker(cbAsync) {
            fs.readFile(stockTestFile,function(err,file) {
                cbAsync(err,file);
            });
        },
        function parseAndPrepareStockTicker(file,cbAsync) {
            var stocksJson = JSON.parse(file);
            if ( stocksJson[ticker] === null ) {
                cbAsync(new Error('Missing ticker property in JSON.'));
                return;
            }
            stocksJson[ticker] = value;
            cbAsync(null,JSON.stringify(stocksJson,null,4));
        },
        function writeTicker(jsonString,cbAsync) {
            fs.writeFile('stocktest.json',jsonString,,function(err) {
                cbAsync(err);
            });
        }
    ],function asyncComplete(err) {
        if ( err ) {
            console.warn('Error updating stock ticker JSON.',err);
        }
        callback(err);
    });
}

最後の反復では、いくつかの bind トリックを使用してこれを短縮し、コールスタックを減らして読みやすさ(IMO)を向上させます。

var fs = require('fs'),
    async = require('async');

function updateJson(ticker,value,callback) {
    var stockTestFile = 'stocktest.json';
    async.waterfall([
        fs.readFile.bind(fs,stockTestFile),
        function parseStockTicker(file,cbAsync) {
            var stocksJson = JSON.parse(file);
            if ( stocksJson[ticker] === null ) {
                cbAsync(new Error('Missing ticker property in JSON.'));
                return;
            }
            cbAsync(null,stocksJson);
        },
        function prepareStockTicker(stocksJson,cbAsync) {
            stocksJson[ticker] = value;
            cbAsync(null,JSON.stringify(stocksJson,null,4));
        },
        fs.writeFile.bind(fs,stockTestFile)
    ],function asyncComplete(err) {
        if ( err ) {
            console.warn('Error updating stock ticker JSON.',err);
        }
        callback(err);
    });
}
14
zamnuts

基本的に、実行にある程度の時間を必要とするnodejs(およびより一般的にはjavascript)関数(I/OまたはCPU処理のため)は通常非同期であるため、イベントループ(簡単にするために、実行されるタスクを継続的にチェックするループです) )は、応答に対してブロックされることなく、最初の関数のすぐ下で関数を呼び出すことができます。 CやJavaなどの他の言語に精通している場合は、非同期関数を別のスレッドで実行される関数と考えることができます(javascriptでは必ずしもそうではありませんが、プログラマは気にする必要はありません)。スレッドは、メインスレッド(イベントループスレッド)にジョブが完了し、結果があることを通知します。

前述のように、最初の関数がジョブを終了すると、そのジョブが終了したことを通知できる必要があり、渡されたコールバック関数を呼び出します。例を作るには:

var callback = function(data,err)
{
   if(!err)
   {
     do something with the received data
   }
   else
     something went wrong
}


asyncFunction1(someparams, callback);

asyncFunction2(someotherparams);

実行フローは、asyncFunction1、asyncFunction2、およびasyncFunction1が終了するまで以下のすべての関数を呼び出します。最後のパラメーターとしてasyncFunction1に渡されるコールバック関数が呼び出され、エラーが発生しなかった場合にデータを処理します。

したがって、2つ以上の非同期関数が終了したときにのみ次々に実行されるようにするには、コールバック関数内でそれらを呼び出す必要があります。

function asyncTask1(data, function(result1, err)
{
   if(!err)
     asyncTask2(data, function(result2, err2)
     {
           if(!err2)
        //call maybe a third async function
           else
             console.log(err2);
     });
    else
     console.log(err);
});

result1はasyncTask1からの戻り値で、result2はasyncTask2の戻り値です。この方法で、必要な非同期関数の数をネストできます。

あなたの場合、updateJson()の後に別の関数を呼び出す場合は、次の行の後に呼び出す必要があります。

console.log("File successfully written");
2
MastErAldo