私的Promise備忘録

昨年の年末にシェル芸勉強会でLTしたヤツのまとめです。
引き続き発表は凄惨なもの(自己責任)となりましたが、個人的には良い勉強になりました。
2016年のクリスマスイブの総決算です。

また、完全に自己流で行ったので、何か気になる点があったら優しくツッコミを入れてくれると助かります。

前提条件

ココで取り上げるのはJavaScriptのPromiseです。
実行した環境としては、 Node.jsのv6.4.0です。
ブラウザでは試してません。対応状況だけ貼っておきます。
Can I use... Support tables for HTML5, CSS3, etc

きっかけ

はじめてNode.jsでの開発を行った際、大量にPromiseの処理が書いてあって混乱したのが、きっかけです。
色々なファイルを色々なところから呼んでいて、どの PromiseがどのPromiseの前後に来て、どう作用して、、、、みたいなのが多くて大混乱しました。
資料自体は結構ネット上にもあったのですが、何だかあまりピンと来なかったりもしたので、一回本腰いれてやってみようと思い、総当たりでやってみました。

Promiseとは

Promiseオブジェクトは処理の延期(deferred)と非同期処理のために使われます。

by  Promise - JavaScript | MDN

Promiseの作成

Promiseは以下のようなカンジで作成します。
Promiseの中に行いたい非同期処理を行う関数を入れます。
関数の1つ目の引数は処理結果が正常な時に呼ぶ関数、2つ目の引数は処理結果がエラーの時に呼ぶ関数です。
基本的に、1つ目の引数はresolveで2つ目の引数はrejectで定義されます。
多分、他のサイトもコレに則っているトコが大半だと思うので、よほど特別なコトが無い限りは、こう書くのが自然であり、無難です。

const testPromise = new Promise((resolve, reject) => {
    //  行いたい非同期処理
});

Promiseの使い方

作成したPromiseは以下のように使います。

const testPromise = new Promise((resolve, reject) => {
    //  行いたい非同期処理
});

testPromise
.then(() => { /* 成功時処理 */ })
.catch(() => { /* 失敗時処理 */ })

具体的な使い方をこれから書いてみます。

resolve・rejectとthen・catchの関係

resolve()が呼ばれた際に実行されるのはthenに渡した関数です。
実行される関数へは引数を1つ渡せます。いくつか渡したい値がある場合には1つのオブジェクトにして渡してあげたりすると良いかなと思います。

reject()が呼ばれた際に実行されるのはcatchに渡した関数です。
こちらも実行される関数へ引数を1つ渡せます。一般的にはErrorオブジェクトを渡してエラー処理を行わせたりするようです。

resolveとrejectは先に発動されたほうのみが実行されます。例えば、resolve(result);reject(result);と書けばthen内の関数のみが実行され、reject(result);resolve(result);と書けばcatch内の関数のみが実行されるイメージです。

また、catchのあとにthenを繋げると、処理の成功・失敗によらず実行される関数を定義することが出来ます。個人的にはfinallyのような処理が出来るという解釈をしています。
これに関しては、引数を取ることが出来ないようです。

下記に軽くサンプルを置きます。

const test = new Promise((resolve, reject) => {
setTimeout(() => {
const result = Math.round(Math.random() * 10) % 2; // 0か1を出力する
if (result === 1) reject(result);
resolve(result);
}, 1000)
});
test
.then((result) => console.log("成功しました。 結果:" + result))  //resolveが呼ばれた時に実行
.catch((result) => console.log("失敗しました。 結果:" + result)) //rejectが呼ばれた時に実行
.then(() => console.log("Fin"))  //毎回実行される。
view raw Promise1.js hosted with ❤ by GitHub

結果:

yuna-no-MacBook-Pro:test yunamiyashita$ node index.js 
成功しました。 結果:0
Fin
yuna-no-MacBook-Pro:test yunamiyashita$ node index.js 
失敗しました。 結果:1
Fin

Promiseを使って非同期処理を同期的に書いてみる

昔のJavascriptだと、非同期処理を同期的に行おうとすると、下記のプログラムのような、俗にいうコールバック地獄というものに陥っていたと思います。

setTimeout(() => {
    console.log("4秒経過");
    setTimeout(() => {
        console.log("7秒経過");
        setTimeout(() => {
            console.log("9秒経過");
            setTimeout(() => {
                console.log("10秒経過");
            }, 1000)
        }, 2000)
    }, 3000)
}, 4000);

Promiseを使うと、チェーン状に繋いで記載することが可能です。
上記のプログラムを書き直してみると、以下のように書くことが出来ます。
説明するのが難しいのですが、この場合resolveが呼ばれた際に実行されるのは、その関数の後に書かれているthen1つが実行されているイメージでしょうか。
個人的にはシェルでいうパイプのようなイメージを持っています。
どのように処理が行われたら分かりやすいかイメージしやすくなればと、setTimeoutで使う時間[ms]をresolveで渡してみました。イメージの参考になれば良いなと思います。

const test = new Promise((resolve, reject) => {
setTimeout(()=>{
console.log("4秒経過");
resolve(3000);
},4000)
});
test
.then((sec) => new Promise((resolve) => setTimeout(() => { console.log("7秒経過");resolve(2000); },sec)))  //sec=3000
.then((sec) => new Promise((resolve) => setTimeout(() => { console.log("9秒経過");resolve(1000); },sec)))  //sec=2000
.then((sec) => new Promise((resolve) => setTimeout(() => { console.log("10秒経過");resolve(); },sec)))        //sec=1000
view raw Promise2.js hosted with ❤ by GitHub

Promiseを作成する関数を用いてみる

また、上記のように同じ構成のPromiseを作成する際には、その作成を関数化させることが可能です。
こちらのほうが、よりスッキリと書けますね。

const timeoutPromise = (sec, msg) => {
return new Promise((resolve, reject) => {
setTimeout(()=>{
console.log(msg);
resolve();
},sec)
});
}
timeoutPromise(4000,"4秒経過")
.then(() => timeoutPromise(3000,"7秒経過"))
.then(() => timeoutPromise(2000,"9秒経過"))
.then(() => timeoutPromise(1000,"10秒経過"))
view raw Promise3.js hosted with ❤ by GitHub

Promise.allについて

Promise.allは下記のようにして使います。
p1〜p4には主に作成されたPromiseを入れます。(Promise以外の処理を入れることも可)
そして、全ての処理が成功した場合にはthenの中の処理が実行され、1つでも処理が失敗すればcatchの中の処理が実行されます。
また、resolveに引数として渡された値はthenが実行される際に配列で渡されます。
配列での格納順はPromise.allに渡した配列に入れた順番に格納されます。

Promise.all([p1, p2, p3, p4])
.then((results) => { /* 成功時の処理 */ })) //results = [r1, r2, r3, r4]
.catch((Err) => { /* 失敗時の処理 */ })

//p1でresolveに渡した引数がr1,p2でresolveに渡した引数がr2,p3でresolveに渡した引数がr3,p4でresolveに渡した引数がr4

説明が難しいので、試しにサンプルを作ってみました。

const testPromise = (num,sec) => {
const random = Math.round(Math.random() * 10); //ランダムで0〜9の数値を返す
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(num + ": random is " + random);
if (random === 3) reject("random is three");
resolve("random is " + random)
},sec);
})
}
const p1 = testPromise(1, 3000);
const p2 = console.log("Not Promise Method");
const p3 = testPromise(2,10);
const p4 = testPromise(3,2);
Promise.all([p1, p2, p3, p4])
.then((results) => results.forEach((result) => console.log("Success: " + result)))
.catch((err) => console.log("Error: " + err))
view raw Promise4.js hosted with ❤ by GitHub

処理が全て成功した場合の一例:

yuna-no-MacBook-Pro:test yunamiyashita$ node index.js 
Not Promise Method
3:  random is 1
2:  random is 1
1:  random is 4
Success: random is 4
Success: undefined
Success: random is 1
Success: random is 1

1つでも失敗した場合の一例:

yuna-no-MacBook-Pro:test yunamiyashita$ node index.js 
Not Promise Method
3:  random is 9
2:  random is 3
Error: random is three
1:  random is 7

処理が全て成功した場合の事例にて、Success: undefinedとなっているのはL13に記載したp2の処理ではresolveが定義されていないため、引数が存在せず、undefinedとなっています。
また私は、Promise.allについて全てのresolveが呼ばれるか、一回でもrejectが呼ばれるかで、thenやcatchの関数が実行されると思っています。
なぜなら、処理が成功した=resolveが呼ばれた処理が失敗した=rejectが呼ばれたという形で判定していると思っているからです。
なので、決してthenの中の関数が実行されるのは格納されている全ての処理が終了したかでは無いので、resolveは適切な場所に入れましょう。

Promise.raceについて

Promise.raceについてはPromise.allと記載したカンジは酷似しています。
違うのはthenの中の関数が呼ばれるタイミングで、こちらは一回でも配列に格納した処理が成功したら呼ばれます。
つまりはresolveが最初に呼ばれたタイミングでthenの中の関数が実行されます。
また、Promise.allでは渡されていた結果値を格納した配列は渡されません。

Promise.race([p1, p2, p3, p4])
.then(() => { /* 処理 */ }))

これに関しては、まだまだ実験中の機能のようです。

これは Harmony(ECMAScript 6) 提案の一部であり、実験段階の技術です。

と今(2017/1)はMDNのサイトに書いてありました。
今の段階のNode.jsではreject&catchが上手く機能しなかったりするので、あまり製品に組み込んだりしないほうが良いのかなと感じています。

参考資料

お世話になった資料です。

developer.mozilla.org

azu.github.io


コレに関しては本当に、試すのも大変だったし、書くのも大変だったし、色々としんどかったです。
クリスマスイブの幸せそうなカップルの横でひたすら処理を試したコトしか覚えていませんね!!
なので、今月はもう少し楽しいコトをやるつもりです。
下らないコマンドとか作りたいです。