#目次
【基礎編】
・Promiseとは?
・実装方法
-各関数内の処理
・おまけ
【発展編】
・Promiseを使った繰り返し処理
-私が陥った悪い例
##【基礎編】
###Promiseとは?
PromiseはJavascriptでの非同期処理を実装する際に用いられるオブジェクトです。
このオブジェクトを使うと、非同期処理が「同期的に」(≒実装してある順番で)実行できます。
例えば何も考えず非同期処理をfor文で繰り返すと、処理の順番が狂いやすくなります。
これは以下の点が影響しています。
・JavaScriptはシングルスレッドで動作している。
・つまり、JavaScriptは同時に2つ以上のことができない。
・一方で非同期処理(APIへのHttpリクエストなど)では、DBなどの処理結果を待っている間、できるタスクを勝手に進めてしまう。
実体験としては、「コンストラクタ定義」→「WebAPIへPOST」→「テキストファイルへ書き込み」という処理をfor文で回した場合、「コンストラクタ定義を連続で回した後」、「WebAPIへPOST」、「値が返ってきた順にテキストファイルに書き込み」という処理結果になったことがありました。結果本来時系列順に書き込まれるデータは毎回ばらばら。。。
こんな事態を解決してくれるのが、Promiseオブジェクトです。
###実装方法
この段落では、Promiseを用いた非同期処理の実装方法に関して解説します。
まず、以下のコードをご覧ください。
var promise = Promise.resolve();
この処理は非同期処理を行う前に必要になります。
Promise.resolve()は()内の引数で解決されたPromiseオブジェクトを返します。
あくまでイメージですが、変数promiseの型宣言レベルのものだと考えてください。
(詳しいことは調査中です。。。)
その後の処理に関しては、以下のコードを御覧ください。
var promise = Promise.resolve();
promise
.then(setBody)
.then(requestDetail)
.then(writeText)
.catch(onRejected);
このコードではsetBody→requestDetail→writeTextの順で処理が実行され、promiseが各関数の処理結果を毎回受け取り、次の関数へ引き渡しています。
".catch(onRejected)"は各関数での処理が失敗した場合に呼び出される関数なので、全ての処理が成功すれば、呼び出されることはありません。
####各関数内の処理
setBody()~writeText()は実装してある順番に実行されます。
前の関数の処理が終わらなければ、次の関数が実行されません。
以下に各関数の基本的な構造を記載します。
function 関数名(){
return new Promise(function(resolve, reject){
//実装部分
resolve(実行結果);
});
}
まず関数名を指定して実行すると、以下の順番で処理が進みます。
1.実装部分に書いた処理を実行
2.成功すればresolve()を実行
3.指定した値が、次に実行する関数に引き渡される。
3で引き渡す値はPromiseオブジェクトとして渡されます。
このままだと少し分かりづらいので、wholeProcess.jsの中から、requestDetail()を例に、詳細を解説します。
function requestDetail(Option){
return new Promise(function(resolve, reject){
reqM(Option, function (error, response, rd) {
var nextData = "Date:"+rd.date
nextData += ",Place:"+rd.place
nextData += ",Group:"+rd.group
resolve(nextData);
})
});
}
この関数はAPIへPOSTを行うためのものです。
OptionにはsetBody()で生成したPOSTを行うための値が入っています
この実装ではPOSTして返ってきたjsonデータ[rd]から任意のデータを抽出、加工して、nextDataとして次の関数に引き渡しています。
無論次の関数でも引数を指定してやれば、nextDataが使用出来ます。
###おまけ
処理が失敗した場合の処理も記載しておきます。
function onRejected(error) {
console.log("error = " + error+"\r\n");
process.exit(1);
}
この処理ではエラー内容を受け取ってコンソールに表示、処理を強制停止します。
強制停止の処理はprocess.exit(1);です。詳細に関しては本記事の趣旨から逸れるため、割愛します。
##【発展編】
###Promiseを使った繰り返し処理
今までの記載内容を踏まえて、繰り返し処理に関しても触れておきましょう。
まず、wholeProcess.jsをベースに、繰り返し処理のサンプルを記載します。
var promise1 = Promise.resolve();
promis1 = promise.then(login)
.then(action1)
.then(function loop(a1result){
return new Promise(function(resolve, reject){
if(count>=dbids.length){
console.log("終了しました。");
process.on('exit', () => {
process.exit(1);
});
}
var promise2 = Promise.resolve(a1result);
promise2
.then(setBody)
.then(requestDetail)
.then(writeText)
.then(function increase(completeData){
return new Promise(function(resolve2, reject){
count++;
resolve(a1result);
})
})
})
.then(loop)
})
繰り返しに関しては、再帰呼び出しを採用しています。
具体的にはpromise2を使用したチェーンメソッドの最後にloop()を再度呼び出すことで、繰り返しを実現しています。
他にもforEach文などを使っても繰り返しは実装できるようですが、この方法が一番分かりやすいのではないでしょうか。
またloop()では、Promise1とPromise2の2つのオブジェクトが存在し、以下の役割を担っています。
Promise1:「setBodyに投げるa1resultを管理する」
→繰り返し処理に必要
Promise2:「各関数の処理結果を管理する」
→loop()内の処理に必要
####私が陥った悪い例
最後に私が陥った悪例を載せておきます。
Promiseに私が出会う前の話です。
for(var i = 0;i<dbids.length;i++){
var option = {url:"Url",
body:{"Id":dbids[i],"hoge": "hogehoge"},
json:true}
request(option, function (error, response, re) {
var content = re.response1
fs.appendFile('result.txt' ,content,'utf8',(err) => {
if(err){
console.log(err);
}else{
console.log('保存しました\n');
}
});
})
}
冒頭でも言いましたが、この処理ではテキストファイルに出力される内容の順番はバラバラになります。
非同期処理はfor文でそのまま回すのは止めましょう。(自戒)
###後記
先に書いた記事は感想文になってしまいましたが、今回は有用性を重視して、解説を細かくしてみました。
私のような初心者目線で、開発中に私が疑問に感じた点を網羅するよう意識して書きましたが、逆に中級者以上の方には面白くない内容になってしまっていると思います。
内容が間違っているなどの指摘があれば、是非いただきたいです。
また、どこか分からない点があれば報告ください。
一緒に考えましょう(笑)
よろしくお願いします。
最後に。
非同期処理の繰り返しが発展編なのは、私が盛大にハマって中々抜け出せなかったからです。
他の初心者の方が非同期処理がきっかけで、JavaScriptを嫌いになりませんように・・・。
そう思ってこの記事を書きました。
JavaScriptはいいぞ。