今回は、JavaScriptで非同期処理を簡潔に記述できるPromiseについて学習をしていきましょう!
「同期処理・非同期処理って何?」
「Promiseってどうやって使うのか分からない」
「Promiseの活用方法やエラーハンドリングを知りたい」
このような内容も含めて、本記事では以下のような構成で解説していきます!
【基礎】「Promise」の使い方
【実践】「Promise」の活用
【実践】エラーハンドリング
【まとめ】「Promise」の使い方まとめ
この記事で、Promiseをしっかり学習してスキルアップを目指していきましょう!
>>> 今すぐ「Promiseの使い方」を知りたい方はこちら
「Promise」とは?
まず最初に「Promise」を理解するための基本的な知識から学習していきましょう。
重要な概念である「同期処理・非同期処理」の理解と、「コールバック」について学びます。
「同期処理」と「非同期処理」について
「promise」を理解するには、大前提として「同期処理」「非同期処理」の理解が必要ですのでおさらいしておきます。
同期処理は、プログラムを上から下へ順番に1つずつ処理をしていくことを意味します。
同期処理 ↓ 同期処理 ↓ 同期処理
しかしながら、途中で時間の掛かる処理があるとそれ以降の処理がすべてストップするという欠点があります。
同期処理 ↓ 同期処理(時間が掛かる処理) ↓ 同期処理(前の処理が終わるまでストップ) ↓ 同期処理(前の処理が終わるまでストップ)
例えば、サーバーからデータを取得するような場合は時間が掛かるので、通信が完了するまで待たなくてはいけません。
このような欠点を解消するために、JavaScriptでは「非同期処理」という仕組みが用意されているわけです。
非同期処理は、時間の掛かりそうな処理を実行したあとに結果を待たずにすぐ次の処理を実行できます。
そして、結果が返ってきたら元の処理を続けることができるのです。
同期処理 ↓ 非同期処理 → 結果が返ってきたら処理を続ける ↓ 同期処理(前の処理を待たなくてもOK) ↓ 同期処理
このような「同期処理・非同期処理」の基本について、まずは慣れておくようにしましょう!
「コールバック」を使ったプログラム
非同期処理の結果を元にして、何らかの処理を実行する方法も合わせて見てみましょう!
例えば、日付データを取得したあとにそのデータを使って西暦を抽出する場合を考えてみます。
関数A(日付データを取得する) ↓ 関数B(日付データから西暦を抽出する)
(この内容なら処理を分ける必要はないのですが、概念を解説するために意図的に分けています)
それぞれの処理を普通に記述すると、以下のようになります。
//日付データを取得する function getDate() { var date = new Date; } //日付データを元に西暦を取得する function getYear(data) { var year = data.getFullYear(); }
「getDate()」が日付を取得する処理で、「getYear()」がその日付から西暦を抽出する処理になります。
ここで重要なのは、日付データを取得しないと「getYear()」が実行できないという点です。
そこで、日付データを元にして処理をするためにgetDate()へ「コールバック」を実装しましょう。
function getDate(callback) { callback(new Date); }
このように、「getDate()」の引数にコールバックを設定すれば関数を指定できるようになります。
すると、次のように実行できるのです。
getDate(function(data) { getYear(data); });
このように記述することで、日付データを取得したあとに「getYear()」を実行できるわけです。
同じ仕組みで、サーバーからデータを取得したあとにそのデータを使って処理を行うようなことも実現できます。
「コールバック地獄」について
「非同期処理」と「コールバック処理」は組み合わせてよく使われるのですが、複雑化しやすいという欠点があります。
先ほどの例は、日付を取得して西暦を抽出するだけだったので問題ありません。
ところが、さらに別の処理が追加されていくと階層構造がどんどん複雑になっていくわけです。
getDate(function(data1) { getYear(function(data2) { getSomething(function(data3) { getAnotherThing(function(data4) { //何らかの処理をする }); }); }); });
こうなるとコードの見通しが悪くなるばかりか、大きなバグを引き起こしやすくなります。
このような状態を「コールバック地獄」と呼びます。
そこで、この問題を解消してくれるのが本記事のテーマである「Promise」という処理なのです!
「Promise」の使い方
この章では、実際に「Promise」を使ったプログラミング手法について見ていきましょう!
基本的な書き方から「then」によるコールバック処理について学んでいきます。
基本的な構文と書き方について
それでは、基本となるPromiseの書き方から学んでいきます。
まず最初に知っておくべきことはPromiseオブジェクトの作り方です!
このPromiseオブジェクトを作成することで、簡潔に非同期処理を実現することができるわけです。
一般的にはnewを使ってPromiseのインスタンスを作成し、その「返り値」としてPromiseオブジェクトを取得します。
var result = new Promise( )
Promise()の引数には関数を設定し、その中で行いたい処理を記述するのが一般的です。
例えば、日付を取得するPromise処理は次のようになります。
var result = new Promise(function(resolve) { resolve(new Date); })
この例では、Promiseの引数に関数を設定して「new Date」で日付を取得しています。
関数の引数「resolve」に取得したい値を設定することで、非同期処理の結果を格納することができるようになります。
(※実際にはエラー処理も必要ですが、これについては後述します)
最終的に変数「result」にはPromiseオブジェクトが格納されることになります。
「then」を使ったコールバック処理を行う方法
Promiseは「then()」を使うことでコールバックのような処理を実現できます。
先ほど作成したPromiseは最終的に変数「result」にPromiseオブジェクトが格納されました。
このPromiseオブジェクトから、例えば日付データを取得して西暦を取得するには次のよう記述できます!
result.then( function(data){ console.log(data.getFullYear()); } );
実行結果
2019
このように「then」の処理内で返されたPromise(data)を使って「getFullYear()」を実行すれば西暦を取得できます。
ちなみに、ES2015(ES6)の書き方であればさらにスマートな記述が可能です。
result.then( (data) => console.log(data.getFullYear()) );
チェーンを使ったPromise処理
この章では、Promise処理をチェーンで繋げて処理する方法について見ていきましょう!
主に、then()をチェーンで処理する方法やall()で並列処理する方法について学んでいきます。
Promise処理を連結する方法
Promise処理のチェーンによる連結処理について、まず最初に冒頭でも解説したコールバック地獄についておさらいしておきましょう!
例えば以下のような状態です。
getDate(function(data1) { getSomething1(function(data2) { getSomething2(function(data3) { getSomething3(function(data4) { }); }); });
階層構造がどんどん複雑化してしまってコードの見通しが悪くなり、デバッグも困難な状態です。
しかし、Promise処理の「then」は以下のようにチェーンで連結することができます!
result .then(function(data) { return getSomething1(item) }) .then(function(item) { return getSomething2(item) }) .then(function(item) { getSomething3(item) })
1つ目のチェーンのあとに続けてthen()の処理を実行し、そのあとにまた別のthen()を繋げるという感じですね。
これにより階層構造が複雑化しにくいため、コードの見通しもよくなるわけです。
また、注意点として「then」を繋げて処理をする場合は、処理の最後に「return」で別のPromiseオブジェクトを返すようにしましょう。
「all」で複数のPromise処理を連結する方法
これまでは、Promiseを使って非同期処理を同期的に連結して実行していました。
しかし、Promiseを使うと複数の非同期処理を同時に実行してすべて終了したあとに別の処理を行うことも可能です。
例えば、「setTimeout()」を使って擬似的に時間の掛かる処理を作ってみます。
//3秒後に実行される処理 function something1() { return new Promise(function(resolve) { setTimeout(function() { resolve('1つ目の処理'); }, 3000) }) }
これは3秒後に実行されるPromise処理で、resolveに指定した単純な文字列が返されます。
そして、同じ処理内容を「1秒後」「0.5秒後」と時間だけ違う関数を3つ作りましょう。
これらのPromise処理を同時に実行して、すべて完了したら次の処理を行うには「all()」を使います。
次のサンプル例を見てください!
Promise.all([ something1(), something2(), something3() ])
このように「all()」の中に配列として同時に実行したい関数を指定するだけです。
すべて完了したあとに次の処理を記述したければ「then」で繋げていけば良いので簡単です。
Promise.all([ something1(), something2(), something3() ]) .then(function(data) { console.log(data); console.log('すべての処理が終了しました!'); })
実行結果
["1つ目の処理", "2つ目の処理", "3つ目の処理"] すべての処理が終了しました!
このように、「then」の中に記述した処理はすべての関数が実行されてから発動します。
注目して欲しいのは「then」で受け取る引数「data」の値です。
実行結果を見ると、配列としてそれぞれのPromiseで指定したresolve()の値が格納されています。
そのため、それぞれの実行結果をまとめて管理できるというメリットもあるので覚えておきましょう!
「Promise」でAjax通信
この章では、Promise処理においてよく使われるAjax通信の作り方について見ていきいましょう!
一般的な「XMLHttpRequest」による手法を詳しく学んでいきいます。
「XMLHttpRequest」による通信方法の書き方
まずは、おさらいとして「XMLHttpRequest()」を使ったAjax通信の方法を見ていきいましょう!
処理の流れとしては、次のとおりです。
インスタンスを作成する ↓ 指定のサーバーと通信を開始する ↓ 通信が正常に終了したかを確認する ↓ サーバーから取得したデータを使う
この流れに沿って、実際にプログラミングすると次のようになります!
//インスタンスを作成する var xhr = new XMLHttpRequest(); //指定のサーバーとの通信を開始する xhr.open('GET', url); xhr.send(); //通信が正常に終了したかを確認する xhr.onreadystatechange = function() { if(xhr.readyState === 4 && xhr.status === 200) { //サーバーから取得したデータを使う } }
ポイントは、「onreadystatechange」を使ってサーバーとの通信が正常に完了しているかを確認することです。
これを行わないと、正しいデータをサーバーから取得できないので忘れないようにしましょう!
「XMLHttpRequest()」を使ったさらに詳しい使い方については、次の記事でまとめているので参考にして見てください!
PromiseでサーバーからGET通信する方法
基本的なAjax通信の方法を把握したところで、Promiseを実装していきましょう!
基本的な考え方は、これまでのPromise処理と同じなので難しくはありません。
今回は、サンプルとしてGitHubに公開されている「JavaScript」のリポジトリ総数を取得してみましょう!
次のサンプル例を見てください!
//GitHubからJavaScriptのリポジトリ総数を取得するURL var url = 'https://api.github.com/search/repositories?q=javascript'; function get(url) { return new Promise(function(resolve) { var xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.send(); xhr.onreadystatechange = function() { if(xhr.readyState === 4 && xhr.status === 200) { var result = JSON.parse(xhr.responseText); resolve( result.total_count ); } } }); }
基本となるプログラムは先ほどとほとんど同じです。
違うのは「XMLHttpRequest()」を使ったコードを丸ごとPromise処理で囲んでいる点です。
そして、GitHubからJSONデータを取得できるので、その中からリポジトリ総数だけを取得します。
あとは、それを「resolve()」に指定すればPromise処理の完成ですね。
実際にコードを実行するには次のように記述します!
get(url) .then(function(response) { console.log("成功!", response); })
実行結果
成功! 385083
このように「then」から受けとる「response」には、リポジトリ総数が格納されています。
エラーハンドリングについて
この章では、Promiseのエラーハンドリングについて見ていきましょう。
基本となる条件分岐について学んでいきます。
Promiseでエラー処理を条件分岐する方法
これまでのPromise処理では、すべて正常に動作した場合の記述だけをしてきました。
しかし、場合によってはPromiseが正しく動作しないケースも少なくありません。
そのため、基本的にはエラーが発生した場合の処理も一緒に記述するべきでしょう。
例えば、前章のAjax通信の場合であれば次のようにエラー処理を記述します。
return new Promise(function(resolve, reject) { ・ ・ ・ xhr.onreadystatechange = function() { if(xhr.readyState === 4 && xhr.status === 200) { resolve('正常に処理されました!'); } else if(xhr.status !== 200) { reject('エラーです!'); } } })
これまで、Promiseが正常に実行された場合は「resolve」を使っていましたが、エラー処理の場合は「reject」を使います。
基本的に「resolve / reject」はペアで一緒に使われるので覚えておきましょう!
実行するには次のように記述します。
get(url) .then(function( response ) { console.log( response ); }, function( error ) { //エラー処理を記述する console.error( error ); })
これまでは「then」の第1引数だけを使っていましたが、第2引数に「reject()」のPromiseが返るようになっています。
そのため、エラー処理を記述する場合は上記サンプルのように第2引数を使いましょう!
「Promise」の使い方まとめ
最後に、Promiseの基本的な使い方をまとめておきます!
Promiseを使うためには、まず最初にPromiseオブジェクトを作成する必要があります。
一般的にはPromiseのインスタンスを作成する際に返り値としてPromiseオブジェクトを取得することができます。
var result = new Promise( )
Promise()の引数には非同期で処理を行いたい関数を記述します。
例えば、Promiseで日付を取得したい場合は次のようになります。
var result = new Promise(function(resolve) { resolve(new Date); }) result.then( function(data){ console.log(data.getFullYear()); } );
Promiseの引数に設定した関数の「resolve」に値を格納することで、非同期で処理した結果をthen()を使って取得することができます。
then()はチェーンで連結することも可能です。
result .then(function(data) { return getSomething1(item) }) .then(function(item) { return getSomething2(item) }) .then(function(item) { getSomething3(item) })
これにより複雑な階層構造を構築する必要がなく、コードの見通しが良くなるわけです。
また、all()を利用することで複数のPromise処理を並列に実行することが可能です。
Promise.all([ something1(), something2(), something3() ]) .then(function(data) { console.log(data); console.log('すべての処理が終了しました!'); })
この場合、thenで得られる値はすべてのPromise処理が終了した結果になります。
まとめ
今回は、非同期処理を完結に記述できる「Promise」について学習しました。
最後に、もう一度ポイントをおさらいしておきましょう!
・「同期処理・非同期処理」の違いと「コールバック」の理解がPromiseには必要
・Ajax通信に関しても同じようにPromiseが利用できる
・エラーハンドリングには「reject()」を使って処理を行う
上記内容を踏まえて、ぜひ自分でもプログラミングに取り入れて活用できるように頑張りましょう!