ES6のPromiseの基本について改めて確認しましたので備忘録として書いておこうと思います。普段はCallbackだけで済ますことが多くあまり積極的に使ってこなかったので、この機会に積極的に使っていこうと思います。
例としてファイルを読み込んで別ファイルに書き出すnodeプログラムで考えていきたいと思います。以下にCallbackだけのパターンとPromiseパターンを考えます。
$ node -v
v8.4.0
1.Callbackパターン
以下がcallbackのみを使ったパターンです。readFileのCallbackの中でwriteFileを呼んでいます。writeFileのCallbackの中で最終的な処理の成否がわかります。
const fs = require("fs");
fs.readFile("ttt.txt", (err,contents) => {
if(err) { console.log(err); return; }
fs.writeFile("ttt2.txt", contents, (err) => {
if(err) { console.log(err); return;}
console.log("書き込み成功!");
})
})
このパタンの悪いところは、処理が長くなるにつれてCallbackの入れ子が深くなり読むに堪えないコードになっていくことです。今回は2段階の深さですが、3段階、4段階になるにつれどんどん汚くなっていきます。Callbackヘルと呼ばれています。Promiseは第一義的にこれを解決してくれます。
2.Promiseパターン
以下がPromiseを使って書き換えたパターンです。ソースコードが2倍近く長くなっていますが、もともとがシンプルなテストコードなので長くなりますが、実際のプログラムではあまり違わないのではないでしょうか。
const fs = require("fs");
function readFile(file){
return new Promise( (resolve,reject) =>{
fs.readFile(file, (err,contents) => {
if(err) {reject(err); return;}
resolve(contents); //※1
})
})
}
function writeFile(file, contents){
return new Promise( (resolve,reject) =>{
fs.writeFile(file, contents, (err) => {
if(err) {reject(err); return;}
resolve("書き込み成功!"); //※3
})
})
}
let promise = readFile("ttt.txt");
promise.then( contents => writeFile("ttt2.txt", contents) ) //※2
.then( mess => console.log("mess="+mess),
err => console.log("error="+err.message) )
まず押さえておきたいのは、PromiseはCallbackの代替ではなく、Callbackヘルを無くすためのものです。(それ以外のメリットもありますが)。非同期処理をPromiseコンストラクタでラッピングして、Promiseオブジェクトを返してくれます。Promiseオブジェクトが表しているのは、非同期処理のcallback実行後の未来の値ということです。Promiseコンストラクタ実行時には、callbackは終了しておらず、未来の値は確定していません(Pendingの状態)。
Promiseは以下の状態を遷移します。
Pending:非同期処理が終了していない
Fulfilled:非同期処理が成功して終了した
Reject:非同期処理がエラーで終了した
PromiseがPending->Fulfilledと変わったときにthen()メソッドが起動されます。以上がPromiseの概略となりますが、以下にソースコードに沿ってもう少し具体的に見ていきます。
まず以下がファイル読み込みの非同期処理をラッピングしたPromiseになります。readFile(file)を呼ぶとPromiseを返します。
function readFile(file){
return new Promise( (resolve,reject) =>{
fs.readFile(file, (err,contents) => {
if(err) {reject(err); return;}
resolve(contents);
})
})
}
Promiseコンストラクタの使い方は以下の通りです。
new Promise (executer)
executerは通常は非同期関数でresolve,rejectの2引数をとります。上のソースのように処理が成功したときにresolve()を呼びFulfilled状態に移ることになります。失敗したときはreject()を呼びReject状態に移ります。
ファイル書き込みのwriteFileについてもreadFileとまったく同様です。
次に作り出したPromiseの使い方です。以下のようにCallbackの入れ子ではなく、チェーンでつないで使えるのがミソです。連鎖した入れ子より、連鎖したチェーンのほうがずっと見通しが良くなります。
let promise = readFile("ttt.txt");
promise.then( contents => writeFile("ttt2.txt", contents) ) //※2
.then( mess => console.log("mess="+mess), //※4
err => console.log("error="+err.message) )
まず promise変数にPromiseを受け取ります。Fulfilled状態に移るとpromise.then()が起動され、※1行のresolve(contents)から、※2行のpromise.then( conntents => ...) へとcontentsの値が渡されます。別の言い方をするとFulfilled状態に変わったということは、未来の値であるcontentsの内容が決まって(解決し)、then()に渡されるということです。
then()の中で何もreturnしなくとも、または値だけreturnしても、今回のようにPromiseをreturnしても、then()はPromiseを作って返します。つまりpromise(...).then(...).then(...)のようなチェーンが成り立ちます。※2行のpromise.then(...)はwriteFile()を呼んで別のPromiseを返しています。上と同じように※4行のpromise.then(...).then( mess =>...)も、※3行目のresolve("書き込み成功!")の値を変数messで受けることになります。
注意すべきは最初のthen()にはエラー処理が無く、2番目のthen()にだけエラー処理があることです。最後にのみエラー処理を置けば途中のエラーの処理も行ってくれるようです。逆に途中のthen()にエラー処理を入れると、それ以降のチェーンの処理が面倒なので無いほうがすっきりして良いです(実はこれは確信をもてるソースがみつかりません。私の経験的な知識にすぎません)
プログラムの実行例を以下に示します。
[sand@www13134uf es6]$ cat ttt.txt
aaaaa
bbbbb
ccccc
[sand@www13134uf es6]$ node node_promise.js
mess=書き込み成功!
[sand@www13134uf es6]$ cat ttt2.txt
aaaaa
bbbbb
ccccc
読み込みファイルがない場合のエラーの実行例です。
[sand@www13134uf es6]$ mv ttt.txt ttt1.txt
[sand@www13134uf es6]$ node node_promise.js
error=ENOENT: no such file or directory, open 'ttt.txt'
書き込みファイルのパーミッションが無い場合のエラーの実行例です。
[sand@www13134uf es6]$ chmod -w ttt2.txt
[sand@www13134uf es6]$ node node_promise.js
error=EACCES: permission denied, open 'ttt2.txt'
以上がPromiseの最も典型的な使用例だと思われます。そのほかにもPromiseやthenにはいろいろな側面があるようですが、大筋だけを掴んでおいて詳細はその都度調べるのがよさそうです。