皆さん、JavaScriptで「アニメーションAを3秒かけて実行したあと、アニメーションBを実行」などと、順番に処理を実行したいとき、どのようにしていますか? 以下のように setTimeout()
を使って実現しているでしょうか?
function serialAnim () {
// ...アニメーションAの処理
setTimeout(() => {
// ...アニメーションBの処理
}, 3000)
}
うーん、ダサいですね。
それとも以下のようにコールバックを利用しているでしょうか?
animA(animB)
function animA (callback) {
// ...アニメーションAを実行した後に、callback(この例ではanimB)を実行する処理
}
setTimeout() は、保守性に問題があることはもちろん、そもそもが「◯秒以降」に実行されるという関数なので正確性にかけますし、コールバック関数は悪くはありませんが、工夫しないとコールバック地獄に陥りがちですよね。
そこで今回は、setTimeout()でもコールバックでもない第三の選択肢として、 Promise クラスを紹介したいと思います。 Promiseをマスターして、美しく処理の実行順序を制御しましょう!!
【Node.js7.5対応】gulp.js/Gruntを卒業すべき3つの理由とgulp非依存の本格タスクランナーの作り方
なお、本記事は先日執筆した上記の記事のスピンオフです。先日の記事ではPromiseを活用してタスクランナーの作り方を詳しく紹介していますので、ぜひ合わせてご覧ください。
https://github.com/sundaycrafts/examples/tree/master/20170210
「非同期」とはなにか
JavaScriptは 非同期 で実行されます。これはJavaScriptの強みでもありますが、同時に コールバック地獄 (原文) に代表される、数々の苦しみを生み出してきた諸刃の剣もあります。
非同期 とはどのようなものなのかイメージがつかない方のために、具体的なコードで実験してみましょう。 以下のコードを async.js
として保存してください。
let msg = ''
// #1
console.log('#1')
msg = msg + 'Hello '
// #2
setTimeout(() => { // 時間のかかる処理
console.log('#2')
msg = msg + "I'm "
}, 500)
// #3
console.log('#3')
msg = msg + 'Jeccy'
// #4
console.log('#4')
console.log(msg)
このスクリプトの最終行 console.log(msg)
の出力結果はどのようになるでしょうか?(ちなみにこのスクリプトはブラウザにjsを読み込ませても実行できます)
- A) 「Hello I’m Jeccy」
- B) 「Hello Jeccy I’m」
- C) 「Hello Jeccy」
Aになってくれると一番嬉しいですよね。
それでは実際に node async.js
として実行してみましょう。 なお、ここではNode.jsを使って動作確認していますが、htmlに同じスクリプトを読み込ませれば、chromeやFirefoxのデバッグ画面なら同様の内容が確認できます。
$ node async.js
#1
#3
#4
Hello Jeccy
#2
非同期でない言語であれば、3行目の setTimeout()
で処理が一旦止まり、スクリプトに書いた順番に文字列が出力されるところですが、 非同期言語であるJavaScriptでは処理が止まらず、 #1
が実行されたあと、即座に #3
以降が実行されてしまいます。
Node(JavaScript)のこの挙動は一見すると直感的でない動作ですが、Node は非同期であることで、DBの読み書きなどの、時間のかかる処理によって全体の処理が滞ることなく、高速なレスポンスを実現しています。タスクランナーでいえば、ファイルの読み書きなどが「時間のかかる処理」に当たります。
- 挙動の確認にはNode.jsを使うと便利です
- htmlを用意して、JavaScriptを書いてブラウザで実行して・・・とするよりも、Node.jsをインストールしていれば、Node.jsからスクリプトを実行してしまったほうが便利です。 コマンドラインから
node ./async.js
と実行してみましょう。
非同期処理をスマートに扱えるPromise
とはいえ、ひとつひとつの処理に順序をつけられないのは困ります。そこで、この強力ながらも扱いづらい非同期処理を扱いやすくするのが Promise です。
実際にPromiseを使用して、先ほどのスクリプトの実行順を制御してみます。
let promise = new Promise((resolve, reject) => { // #1
console.log('#1')
resolve('Hello ')
})
promise.then((msg) => { // #2
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('#2')
resolve(msg + "I'm ")
}, 500)
})
}).then((msg) => { // #3
console.log('#3')
return msg + 'Jeccy.'
}).then((msg) => { // #4
console.log('#4')
console.log(msg)
}).catch(() => { // エラーハンドリング
console.error('Something wrong!')
})
実行してみます。 今度は期待通りの結果が帰ってきました。
$ node async.js
#1
#2
#3
#4
Hello I'm Jeccy.
実際の挙動を確認したところで、Promiseによって処理が制御される様子を詳しく見ていきましょう。
- 用語のおさらい
- ここまでも何度かメソッドやコールバックといった用語が登場していますが、Promiseの説明ではこれらの用語を多用するので、改めてここで説明をしようと思います。たくさん難しい用語がありますね・・・。オブジェクト は関数や配列や文字列などの実体を指します。
初期化 はある関数を
new
をつけて実行することを指します。普通はnew
をつけないで実行した場合と、実行結果が異なります。クラス とは、最初は
new
をつけて実行されることを前提とした関数(オブジェクト)と思って差し支えないかと思います。インスタンス とはクラスが生成したオブジェクトです。クラスは関数なので、実行されると何らかの値を返します。その返された値を指します。大抵関数を内包したオブジェクトであることが多いです。
メソッド とはクラスやインスタンスに内包されている関数です。特に、クラスを初期化しなくても、クラスオブジェクトが最初から内包していて使える関数を スタティックメソッド 、 クラスを初期化した返り値であるインスタンスが使える関数を インスタンスメソッド と呼びます。
コールバック とは
new Promise(fn)
のようにあるクラスや関数を実行した時に渡して、その関数やクラス自身が実行する関数のこと(new Promise(fn)
の中で言えばfn
のこと )です。 コールバック関数 とよく表記ゆれします。
1行目から4行目 — Promiseインスタンスの生成
let promise = new Promise((resolve, reject) => { // #1
console.log('#1')
resolve('Hello ')
})
new Promise()
はインスタンスとしてpromiseオブジェクトを返します。promiseオブジェクトには以下の3つの状態があります。
- Fulfilled – コールバック関数内でresolve()が呼ばれた時
- Rejected – コールバック関数内でreject()が呼ばれた時
- Pending – 初期状態
このpromiseオブジェクトの状態は、 new Promise()
のコールバックで、 resolve()
(第一引数に渡される関数)を実行したか reject()
(第二引数に渡される関数)を実行したかで確定します。そしてこの状態は一度確定すると、その後変化することはありません。
つまり、コールバック内で resolve()
を呼んだあとに reject()
を呼んでも意味がないということです。
なぜpromiseオブジェクトが内部的に持っている状態を理解しなくてはいけないのでしょうか? それはこのpromiseオブジェクトの状態が変化したタイミングで、次に実行されるメソッドが異なるからです。
6行目以降 — Promise Chain
promise.then((msg) => { // #2
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('#2')
resolve(msg + "I'm ")
}, 500)
})
}).then((msg) => { // #3
console.log('#3')
return msg + 'Jeccy.'
}).then((msg) => { // #4
console.log('#4')
console.log(msg)
}).catch(() => { // エラーハンドリング
console.error('Something wrong!')
})
promiseオブジェクトは、 then()
と catch()
という2つのメソッドを持っており、promiseオブジェクト内部の状態が Fulfilled になると、 then()
メソッドのコールバックが呼ばれ、逆に Rejected になると catch()
メソッドのコールバックが呼ばれます。
つまり、実質的に new Promise()
のコールバック内で resolve()
を実行すれば then()
メソッドのコールバックが呼ばれ、 reject()
を実行すれば catch()
メソッドが呼ばれるということです。
また、 then()
メソッドはコールバックがどのような値を返したとしても、すべてpromiseオブジェクトに包んだ返り値を返します。 つまり、 then()
メソッドは、実行すると then()
メソッドを持ったpromiseオブジェクトを返すということです。この性質によって、promiseチェインは実現しています。
また、このpromiseオブジェクトに包まれた返り値は、新しく生成されたpromiseオブジェクトの then()
メソッドのコールバックに渡され、利用することが可能です。例では各処理の中で渡されている msg
という引数がそれに当たります。
ところで、 #2
をみると、 then()
メソッドのコールバック内で、promiseオブジェクト自体を生成して返しています。
promise.then((msg) => { // #2
return new Promise((resolve, reject) => {
「 then()
メソッドがコールバックの返り値をpromiseオブジェクトに包んで返すなら、コールバックがpromiseオブジェクトそのものを返した場合はどうなるんだろう?」と思いませんか? promiseオブジェクトが入れ子になって正常に動かなくなりそうですが、そんなことはありません。試しに
#3
の処理を、promiseオブジェクトを返すように書き換えてみましょう。
}).then((msg) => { // #3
console.log('#3')
return new Promise((resolve, reject) => {
resolve(msg + 'Jeccy.')
このように処理を書き換えたとしても、結果は同じになります。 then()
メソッドのコールバックの中で、promiseオブジェクトを返した場合、 resolve()
実行するまでは次の処理に進まないので、自分の指定したタイミングで次の処理に移れるようになりました。
以上のことから、 #2
のように promiseチェイン内で時間のかかる処理をしたい場合は、 then()
メソッドのコールバックの中で新しいpromiseオブジェクトを作成し、それを返却するようにします。
最後に、 catch()
メソッドで、それより上で実行された処理中に起きるエラーをキャッチ(エラーハンドリング)します。 エラーハンドリングをしないと、Node v7から怒られるようになったのでご注意ください。
終わりに 実際の利用例・ブラウザ対応状況
Promiseの便利さがわかっていただけたでしょうか。 Promiseを使いこなせるようになると、非同期言語であるJavaScriptをより効果的に使いこなせるようになるので、ぜひ積極的に使ってみてください。
ブラウザ対応状況はCan I useによると、 IEさんは11まで安定の非対応 ですので、クライアントサイドですとpolyfillを使って利用する事になります。
Promiseのpolyfillとしては、 bluebird や、軽量な q などが人気です。 どちらもECMAScriptの仕様に沿って実装されているので、サポートされている機能であれば、どちらも使用方法に差はありません。 こちらの使い方はまたどこかでご紹介したいと思います。
また、もっとPromiseを知りたい方は、 JavaScript Promiseの本 が参考になります。
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。