JavaScriptのPromiseについて調べる
Promiseオブジェクトの使い方を少しずつ紐解いていきました…
筆者はC#やC++がメインのため、ファイル読み込みなどは同期処理できないと何とも書きづらい性分の人間です。そういった人からすると、JavaScriptの非同期的な書き方(コールバック地獄?)はつらいのです。ただ、この悩みは多くの方々が持っているようで、すでに解決策が提案されているようです。それが、ECMAScript2015(ES6)から導入された、Promiseオブジェクトです。ちなみに、後日執筆すると思いますが、TypeScript 2.1から、es5をターゲットした場合でもasync/awaitが使えるようになりました。ただ、このasync/awaitは、Promiseのシンタックスシュガーのため、そもそもPromiseの使い方がわかっていないと、使い方がよく分かりませんでした。そのため、まずは、Promiseの使い方について学びたいと思います。
調べていて気付いたのは、WinRTをC++/CXから使うときにほぼ必須のあれかーってやつです…(私の心が折れたやつです…)
Promiseが使える環境
さて、PromiseはES6の仕様ということで、現在の対応状況について調べてみます。なお、この記事ではブラウザ上で動作させることを前提に進めていきます。
Can I use…によると、この記事を執筆時点で、IE11以外のブラウザでは大体対応しているしているようです。IE11でもPromiseが使えるようなライブラリが多く存在しているようで、実質使っても問題ないといってよいでしょう。IE11で使う場合は以下のライブラリが良いようです。
このページの中ごろに使い方が参照の仕方が書いてあります。とりあえず、
1 2 3 |
<script src="https://www.promisejs.org/polyfills/promise-7.0.4.min.js"></script> |
と、ページの処理ソースの前に読み込んでおけば問題ないようです。「7.0.4」はバージョン番号のため、更新されると思います。
TypeScriptのasync/awaitを使った場合、IE11では上記のライブラリを読み込んでおかないとうまく動作してくれませんでした。(問題ないという記事も見かけたため、もう少し調べる必要がありそうです)
ちなみに、今回調べていて初めて知ったのですが、「あるブラウザでサポートされていない機能をサポートするための機能の実装コード」のことを、Polyfillと言うようです (wikipediaの冒頭を訳しました)。
使い方
Promiseを使うといろいろと便利なことができるようですが、ひとまず、非同期のコールバック処理を同期的に書くような書き方について調べていきたいと思います。
1秒(1000ミリ秒)ごとに3つのメッセージを出力するような以下の処理を題材としていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
console.log("Message1:" + new Date().toString()); window.setTimeout( function(){ console.log("Message2:" + new Date().toString()); window.setTimeout( function(){ console.log("Message3:" + new Date().toString()); } ,1000) },1000 ); |
これを実行すると、以下のような出力が得られます。
1 2 3 4 5 |
Message1:Wed Mar 08 2017 12:12:53 GMT+0900 (東京 (標準時)) Message2:Wed Mar 08 2017 12:12:54 GMT+0900 (東京 (標準時)) Message3:Wed Mar 08 2017 12:12:55 GMT+0900 (東京 (標準時)) |
さて、これをPromiseに書き換えると以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function mes2() { return new Promise(function (resolve) { window.setTimeout( function () { console.log("Message2:" + new Date().toString()); resolve(); }, 1000); }); } function mes3() { return new Promise(function (resolve) { window.setTimeout( function () { console.log("Message3:" + new Date().toString()); resolve(); }, 1000); }); } // ここから実行 console.log("Message1:" + new Date().toString()); mes2().then(mes3); |
…複雑になった…
正直、自分で書いていてもよくわからなくなってきますが、とりあえず、こんな感じになるようです。
少しずつ、紐解いていく…
Promiseオブジェクトの動作を、少しずつ紐解いていこうと思います。
Promiseオブジェクトの生成
簡単な例として、コンソールに文字列を出力してみます。
1 2 3 4 5 6 |
var p = new Promise(function() { console.log("Message1"); }); |
Promiseの引数には、実行する関数を渡します。関数の引数には上記のスクリプトを実行すると、以下のように出力されるはずです。
1 2 3 |
Message1 |
どうやらPromiseは、オブジェクト生成時に渡された関数を実行するようです。次に、以下のようにしてみます。Promiseオブジェクトのthenに「Message2」と出力する関数を渡します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var p = new Promise(function() { console.log("Message1"); }); p.then( function() { console.log("Message2"); } ); |
これを実行すると、
1 2 3 |
Message1 |
としか、実行されません。残念…どうやら、then以降を実行できるようにするためには、Promiseのコンストラクタに渡した関数内で適切な処理をする必要があるようです。実は、Promiseに渡す関数には、引数として、成功した際に呼び出す関数が渡されます。それを受け取り、呼び出す必要があります。Promiseに最初に渡した関数を以下のように書き換え、その関数内で呼び出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var p = new Promise(function(resolve) { console.log("Message1"); resolve(); }); p.then( function() { console.log("Message2"); } ); |
引数の「resolve」とその呼び出し「resolve();」がポイントです。このスクリプトを実行すると以下のように表示されます。
1 2 3 4 |
Message1 Message2 |
ちなみに、p.then(~);の文は、別に処理を待っているわけではありません。そのため、例えば以下のように書くと、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var p = new Promise(function(resolve) { console.log("Message1"); resolve(); }); p.then( function() { console.log("Message2"); } ); console.log("Message3"); |
以下のように出力されます。
1 2 3 4 5 |
Message1 Message3 Message2 |
このように、Promiseオブジェクトが処理をブロックするわけではありません。1→2→3のようにしたい場合は、thenをつなげていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var p = new Promise(function(resolve) { console.log("Message1"); resolve(); }); p.then( function() { console.log("Message2"); } ).then( function() { console.log("Message3"); } ); |
thenの振る舞いは、Promiseオブジェクトを返した場合とそうでない場合で若干挙動が異なるのか、その辺、もう少し調べてみますが、ひとまずこういった形で使えるようです。
then 以降の関数に引数を渡す
resolve関数を呼び出す際に引数を渡すと、thenに渡した関数の引数として受け取ることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var p = new Promise(function(resolve) { console.log("Message1"); resolve("Message1のあとに呼び出し"); }); p.then( function(mes) { console.log("Message2:" + mes); } ); |
このスクリプトを実行すると
1 2 3 4 |
Message1 Message2:Message1のあとに呼び出し |
と出力されます。これらを利用すれば、コールバック地獄なしに、同期的に書けそうです。
Promiseを使った書き方のまとめ (テンプレート)
Promiseの使い方は以下の通りになると思います。
- 非同期処理をする関数は、Promiseオブジェクトを返す。
- 次の処理に移行する際には第一引数の関数を呼び出す。
- 非同期処理をする関数を、thenでつないでいく。
コードにすると…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// Promiseオブジェクトを返す関数を作成 function asyncFunc1() { return new Promise(function(resolve) { //コールバック関数 var f = function() { //処理 resolve(); }; // 終了時の関数登録と、非同期処理の開始 }); } function asyncFunc2() { return new Promise(function(resolve) { //コールバック関数 var f = function() { //処理 resolve(); }; // 終了時の関数登録と、非同期処理の開始 }); } function asyncFunc3() { return new Promise(function(resolve) { //コールバック関数 var f = function() { //処理 resolve(); }; // 終了時の関数登録と、非同期処理の開始 }); } // thenでつないでいって呼び出す。 asyncFunc1(). then(asyncFunc2). then(asyncFunc3); |
この場合、最後のasyncFunc3は、Promiseオブジェクトを返す必要はありませんが、一応。
ちなみに、Promiseでは、処理に成功した場合と失敗した場合とで処理を変更することもできます。それについては、またの機会に書きたいと思います。