こんにちは。ECF Techブログ
担当 Michiharu.Tです。
今回はJavaScriptにおける非同期処理に欠かせないPromiseについて、いろいろまとめてみたいと思っています。解りやすさを重視し、なるべくシンプルなプログラムにしたつもりですので、お役に立てれば幸いです。
対象読者
本記事は下記のような方を対象としております。
- JavaScriptの基本的な知識がある方
- プログラミングの基本的な知識がある方
非同期処理
それでは早速いきましょう。まずは非同期処理についてです。非同期処理とは、関数などのある処理の完了を待たずして、プログラムの続きを実行するような処理の仕方のことです(下図参考)。
上図のように、非同期処理を持たない処理を便宜的に「同期処理」と呼ぶことにします。
Webアプリケーションの開発などにおいては、サーバーなどからデータを取得する処理を記述する際など、この非同期処理が用いられることが一般的です。
さらには、いくつかの非同期処理を順番に実行したいというケースも往々にして出てきます。このような処理に役に立つのがPromiseです。
非同期処理が順序良くできない例
Promiseの説明を始めるにあたり、次のようなケース(プログラム)を考えてみたいと思います。
プログラム1
function asyncFunc1(){ console.log("処理1開始"); //1秒後に「処理1完了!」を表示 setTimeout(() => { console.log("処理1完了!"); },1000); } function asyncFunc2(){ console.log("処理2開始"); //1秒後に「処理2完了!」を表示 setTimeout(() => { console.log("処理2完了!"); },1000); } function asyncFunc3(){ console.log("処理3開始"); //1秒後に「処理3完了!」を表示 setTimeout(() => { console.log("処理3完了!"); },1000); } //非同期メソッドの呼び出し asyncFunc1(); asyncFunc2(); asyncFunc3();
このプログラムには3つの非同期処理を模した関数(asyncFunc1~3)があります。
それぞれの関数は
- 「処理X開始」と表示
- 1秒待つ
- 「処理X終了!」と表示
というシンプルな処理です。また、このプログラムで実現しなければならない処理は次のようなものだとします。
- asyncFunc1を呼び出す
- asyncFunc1の処理が完了したら、asyncFunc2を呼び出す。
- asyncFunc2の処理が完了したら、asyncFunc3を呼び出す。
ですが、この処理を実行すると次のような実行結果となります。
処理1開始 処理2開始 処理3開始 処理1完了! 処理2完了! 処理3完了!
処理1の完了を待たずして、処理2や処理3が実行されてしまっていることがわかります。
コールバックの活用
この不具合の解決方法として長らく利用されてきた方法が、コールバックを活用した処理です。プログラム例を示します。
プログラム2
//引数を追加 function asyncFunc1(nextFunc){ console.log("処理1開始"); setTimeout(() => { console.log("処理1完了!"); //下記を追加 nextFunc(); },1000); } //引数を追加 function asyncFunc2(nextFunc){ console.log("処理2開始"); setTimeout(() => { console.log("処理2完了!"); //下記を追加 nextFunc(); },1000); } function asyncFunc3(){ console.log("処理3開始"); setTimeout(() => { console.log("処理3完了!"); },1000); } //非同期関数の呼び出し(処理変更) asyncFunc1( function(){ asyncFunc2( function(){ asyncFunc3(); }); });
プログラム1との違いについてコメントに付記しています。
非同期関数の呼び出し部分では、asyncFunc1と2に追加した引数に関数を指定しているため、関数が入れ子になり、わかりづらくなっています。図で整理すると次のような状態です。
実行すると次のような結果になります。
処理1開始 処理1完了! 処理2開始 処理2完了! 処理3開始 処理3完了!
各関数が順番に実行されており、実現したいことはできています。ですがこの方法は、順番に実行したい非同期の関数が増えるたびにどんどんと入れ子が深くなる という問題があります。これは通称「コールバック地獄」と呼ばれ、あまり好まれない手法です。
Promiseオブジェクト
上記のコールバック地獄の問題を解消してくれるのがPromiseオブジェクトです。Promiseオブジェクトは、非同期処理を同期的に処理できるしくみを提供してくれます。
下記にPromiseオブジェクトの利用例を示します。
プログラム3
var myfunc = new Promise( //引数には2つの引数(resolve,reject)をとる関数を指定 function(resolve, reject){ //この中に非同期で実行したい処理を記述 let success = true; if( success ){ //非同期処理が成功した場合は、resolveを呼び出す resolve("成功!"); } else { //非同期処理が失敗した場合は、rejectを呼び出す reject("失敗"); } } ); //myfunc実行後の処理登録 myfunc.then( function(data){ console.log(data); } );
このプログラムを実行すると、下の実行結果となります。
成功!
Promiseオブジェクト生成の概要はソース中のコメントの通りです。下記にもまとめます。
- new Promiseの引数には、
resolve
とreject
を引数にとる関数(以下、非同期処理関数)を指定する。 - 非同期処理関数内に非同期に実行したい処理を記述する。
- 非同期処理が成功した場合は、resolve関数を呼び出す。
- 非同期処理が失敗した場合は、reject関数を呼び出す。
他にも次のポイントについて述べておきます。
- 非同期処理の成功/失敗は、プログラマ自身が適切に作成・判断します。上記の例では、単にbooleanのフラグで成功か失敗かを判断しています。上記のプログラム例は、5行目の
let success = true
をlet success = false
にすると、実行結果が変化します。 - 上記の例では、resolveおよびrejectを呼び出す際に文字列(
成功!
や失敗
)を引数に渡していますが、いずれの関数も任意のオブジェクトを引数として渡すことができます。
次にthen関数の呼び出し部分についてです。then関数では非同期処理関数が成功、または失敗した場合のコールバック関数を設定することができます。書式的に書くと次のようになります。
Promiseオブジェクト.then(成功時に実行する関数,失敗時に実行する関数);
これらの関数は、非同期処理関数内でresolve
およびreject
のどちらが呼び出されるかによって、下図のように動作します。
なお、非同期処理部分(3行目からの関数)が実行されるタイミングは、Promiseオブジェクトを作成した時です。ですが、その実行後にthenを使ってコールバック関数を登録したとしても、そのコールバック関数は正しく呼び出されます。
Promiseのメソッドチェーン
Promiseについての説明が少し長くなりましたが、本題に戻りたいと思います。元々の目的は下記プログラムの非同期処理関数(asyncFunc1~asyncFunc3)を順番に(前の関数の処理が完了してから)呼び出すことでした。
function asyncFunc1(){ console.log("処理1開始"); //1秒後に「処理1完了!」を表示 setTimeout(() => { console.log("処理1完了!"); },1000); } function asyncFunc2(){ console.log("処理2開始"); //1秒後に「処理2完了!」を表示 setTimeout(() => { console.log("処理2完了!"); },1000); } function asyncFunc3(){ console.log("処理3開始"); //1秒後に「処理3完了!」を表示 setTimeout(() => { console.log("処理3完了!"); },1000); }
この問題もPromiseオブジェクトのthenを利用することで解決できます。プログラム例を示します。
//引数にresolve,rejectを追加 function asyncFunc1(resolve, reject) { console.log("処理1開始"); setTimeout(() =>{ console.log("処理1完了!"); //resolveの呼び出し resolve(true); }, 1000); } //引数にresolve,rejectを追加 function asyncFunc2(resolve, reject){ console.log("処理2開始"); setTimeout(() =>{ console.log("処理2完了!"); //resolveの呼び出し resolve(true); }, 1000); } //引数にresolve,rejectを追加 function asyncFunc3(resolve, reject){ console.log("処理3開始"); setTimeout(() =>{ console.log("処理3完了!"); //resolveの呼び出し resolve(true); }, 1000); } //非同期処理開始 //(resolve,reject)を持つ関数asyncFuncを引数として //Promiseコンストラクタを呼び出し、オブジェクトを生成 new Promise(asyncFunc1) .then(function(){ //(resolve,reject)を持つ関数asyncFunc2を引数として //Promiseコンストラクタを呼び出し、オブジェクトを生成 return new Promise(asyncFunc2); }) .then(function(){ //(resolve,reject)を持つ関数asyncFunc3を引数として //Promiseコンストラクタを呼び出し、オブジェクトを生成 return new Promise(asyncFunc3); });
実行結果
処理1開始 処理1完了! 処理2開始 処理2完了! 処理3開始 処理3完了!
実行結果を見ると、意図した順番に関数が呼び出されていることがわかります。
それではプログラムを順に見てみましょう。ポイントの1つ目は、asyncFunc1~3に引数resolve
、reject
を追加したことです。この2つの引数を追加したことで、Promiseコンストラクタの引数に渡せる関数になりました。
3つの関数を作成後、非同期処理を開始するにあたり
new Promise(asyncFunc1).then(...)
となっています。asyncFunc1
関数をPromiseコンストラクタの引数として渡していますので、asyncFunc1の処理が完了するとthenメソッドに登録している関数が呼び出されます。
そのthen関数に登録している関数は次のようになっています。
function(){ //(resolve,reject)を持つ関数asyncFunc2を引数として //Promiseコンストラクタを呼び出し、オブジェクトを生成 return new Promise(asyncFunc2); }
asyncFunc1が完了するとこの関数が呼び出されます。処理内容はasyncFunc2関数をPromiseコンストラクタに渡してオブジェクトを生成し、それを戻り値として返す処理になっています。
thenに登録した関数内で、Promiseオブジェクトを戻り値として返すと、thenの戻り値自体もそのPromiseオブジェクトとなります。したがって、下のようにthenを数珠つなぎに書いていくことができます。
new Promise(...) .then(...) .then(...)
このような記述は特に「メソッドチェーン」と呼ばれます。
このメソッドチェーンを活用することで、非同期処理を実現したい関数(asyncFunc1~3)を順番に実行しています。下図に一連の内容をまとめています。
おわりに
書いていたらかなり長くなってしまったので、本日は以上とさせていただきます。最後までご覧くださりありがとうございます。基本的な内容ではありますが、少しでも参考になれば幸いです。まだPromiseの使い勝手について書ききれていない部分がありますので、また記事を追加していきたいと思います。よろしくお願いします。
合同会社イー・シー・エフでは、子ども向けプログラミングなどの教育講座を実施しています。プログラミング教室の案内や教育教材の情報、また関連するご相談・問い合わせにつきましては下記よりご確認ください。