【JavaScript】 Promiseを一から学ぶ Promiseで作るモダンな非同期処理

JavaScriptの特徴的な動作として知られる「非同期処理」。

よくわからないで使っていると予期しない動きをしてしまうため、JavaScriptを学ぶうえで大きな障害になることもあるクセモノです。

今回は、EcmaScript2015から導入された「Promise」オブジェクトを使って非同期処理をシンプルに書く方法をご紹介します。

Promiseを上手に使えるようになれば、自分の期待どおりの動きを実現することができるようになるはず。ぜひマスターしてください!

Promiseとは?

  • EcmaScript2015から導入された、比較的新しい技術
  • 非同期処理を行う際に使い、「成功時」「失敗時」の処理を明示的に切り分けて書くことが可能
  • 非同期処理をメソッドチェーンで書くことができるようになるので、ソースの可読性が上がる

非同期処理について

JavaScriptの特徴的な動作のひとつとして、「非同期処理」があります。

非同期処理とは、プログラムの処理を未来に先送りしておき、条件が合致したタイミングで解決させる処理のこと。

処理を一定時間遅らせる」「特定のイベント発生時に処理を走らせる」など、レスポンシブな挙動が求められるJavaScriptにおいては必須の機能です。

Promise以前の非同期処理

一見すると便利そうな非同期処理ですが、使い方を誤るとバグの温床になることも。

とくに、JavaScriptに慣れていない人にとっては、思いどおりの挙動にならずに四苦八苦するポイントでもあります。

このような非同期処理のタイミングを操作するための処置として、主流な方法のひとつが「コールバック関数」を用いる方法。

関数をコールバックとして引数に渡すことで、非同期処理の完了後のタイミングで、意図した処理をおこなうことができるというものです。

ただし、このコールバック関数を使った方法は、複数の処理をおこなう場合にネストが深くなりやすく、非常にコードが読みにくくなるという欠点があります。

asyncFunc1( response => {
asyncFunc2( response => {
asyncFunc3( response => {
asyncFunc4( response => {
...
});
});
});
});

非同期処理の結果をコールバックで受け取って処理をする...というコードはJavaScriptでは必要な場面が多いのですが、上記のようにネストが深い関数は保守性の面でも問題があるので避けたいところ。

このように、ネストが深くなりコードの保守が困難な状態を指して「コールバック地獄(callback hell)」あるいは、ネストが横につきだす様子から「破滅のピラミッド(pyramid of doom)」と呼ぶことがあるようです。

Promiseを使った非同期処理

そこで、非同期処理を扱う際に便利なのが、今回紹介する「Promise」です。

Promiseを使うと、煩雑になりやすい非同期処理が簡潔なコードになります。

asyncFunc1()
.then(() => { asyncFunc2() })
.then(() => { asyncFunc3() })
.then(() => { asyncFunc4() })
...
.then(() => { asyncFuncX() });

処理の順番が直感的に追いやすく、非常にすっきりして見やすくなりました。

このように、Promiseを使うことで、非同期処理を使った処理をシンプルに書くことができるようになります。

Promiseのライフサイクル

Promiseオブジェクトの基本的なライフサイクルは上記のとおりです。

Promiseオブジェクトは初期化された時点で"pending"というステータスになっています。

この状態から、成功時は"fulfilled"、失敗時は"rejected"とそれぞれステータスが変化し、変化したステータスはオブジェクトが破棄されるまで戻りません

そして、このPromiseオブジェクトのステータスに対応して、それぞれ成功時、失敗時のメソッドが呼ばれることで処理の分岐を実現します。

Promiseの基本構文

つづいて、Promiseオブジェクトを使用した処理の基本構文を見ていきましょう。

基本的な流れは以下のとおり。

  1. Promiseオブジェクトを初期化する。(new Promise())
  2. 関数内で、Promiseオブジェクトを返す
  3. 関数呼び出しの際に、
    .then()には成功時の処理
    .catch()には失敗時の処理
    .finally()には必ず実行したい処理を記載する。
function promiseFunction(value) {
const promise = new Promise(function(resolve, reject) { //Promiseオブジェクトを初期化する
setTimeout(function() {
if (value !==null) {
resolve(true);
} else {
reject(false);
}
}, 1000);
});
return promise; //Promiseオブジェクトを返す
}

promiseFunction()
.then(function(response) {
(成功時の処理)
})
.catch(function(error) {
(失敗時の処理)
})
.finally(function() {
(処理結果に関わらず必ず実行したい処理)
});

よくあるtry, catch, finallyの例外処理にそっくりなので、覚えやすいかと思います。

Promiseオブジェクトが持つ非同期処理メソッド

Promiseを扱ううえで知っておきたいメソッドがいくつかあります。

個人的には、これらは以下の3つに分けることができると思っています。
 

  • Promiseオブジェクト初期化時のメソッド
  • 単体のPromiseオブジェクトに使うメソッド
  • 複数のPromiseオブジェクトを引数にとるメソッド

それぞれ順番に見ていきましょう。

Promiseオブジェクト初期化時のメソッド

Promiseオブジェクトを初期化する際に使用することがあるメソッドは以下のふたつ。

  • .resolve()
  • .reject()

これらのメソッドは、「解決済みステータス(fulfilled/rejected)」になっているPromiseオブジェクトを扱いたい場合に使います。

.resolve()は、「成功ステータス(fulfilled)」になっているPromiseオブジェクトを作成するメソッド。

解決済みのPromiseオブジェクトを用意し、.then()などのメソッドでチェーンすることで、総数のわからない複数のPromiseオブジェクトを順次処理する場合などに役立ちます。

async dispatchMultiTasks(asyncTasks) {
// 解決済みPromiseオブジェクト用意
let promise = Promise.resolve();

// 配列に格納した処理の逐次処理
asyncTasks.forEach(function(task, index){
promise = promise.then(async function() {
await task()
.then(function(result) {
...
})
.catch(function(error) {
...
});
});
});
}

.reject()は、.resolve()とは逆に「失敗ステータス(rejected)」のPromiseオブジェクトを作成するメソッドです。

単体のPromiseオブジェクトに使うメソッド

Promiseオブジェクトを扱ううえで特に使用頻度が高いのがこちらの3つのメソッドです。

  • .then()
  • .catch()
  • .finally()

これらは、いずれもpromiseオブジェクトに対して実行されるメソッドです。
promiseオブジェクトの状態によって.then()あるいは.catch()が実行されるのは前述のとおり。

ポイントとなるのは、これらの処理はメソッドチェーンにしてつなげて書くことができるということです。

Promiseを使った非同期処理では、関数の結果としてPromiseオブジェクトを返すルールなので、続けておこないたい処理をメソッドチェーンとして連続して書くことができます。

複雑な非同期処理をシンプルに書くうえでよく使うことになるので、上記メソッドは必ず覚えておきましょう。

複数のPromiseオブジェクトを引数にとるメソッド

最後に、複数のPromiseオブジェクトを引数にとる以下のふたつのメソッドです。

これらのメソッドは、Promiseオブジェクトを返す処理を配列として引数にとることで、非同期処理を同時に実行することができます。

  • .all()
  • .race()
async dispatchAllTasks(asyncTasks) {
// 変数として代入した各関数を呼び出して配列に格納
let newTasks = asyncTasks.map(function(task, index) {
return task().then(function(result) {
...
});
});

// 配列イベントの一斉処理
await Promise.all(newTasks)
.then(function() {
...
})
.catch(function(error) {
...
});
}

どちらも似たようなものなのですが、「.all()はすべての非同期処理の解決を待って次の処理へ移る」「.race()はいずれかの非同期処理が解決した段階で次の処理へ移る」という違いがあります。

async/awaitで非同期処理をコントロール

Promiseを使った非同期処理を扱う場合にぜひ覚えておきたいのが、「async / await」の使い方です。

axiosなどを使ってサーバーサイドとの連携をおこなう処理を書く方であれば、目にする機会も多いのではないでしょうか。

async関数のなかでawaitキーワードを使うと、続く処理の実行を停止して、非同期処理の解決を待たせることができます。

処理タイミングのわかりづらい非同期処理を同期処理のように扱うことができるので、手軽に非同期処理の解決タイミングやその後の処理をコントロールすることが可能に。

サーバーサイドとの連携時に、データの取得が完了するのを待ってからローディング表示を解除したい場合など、非同期処理のコントロール時に重宝します。

$("#api-btn").on("click", async function() {
showLoading();

await $.ajax({
headers: null,
type: 'GET',
url: "/api/fetchData",
contentType: 'application/json; charset=utf-8',
datatype: 'json'
})
.done(async function(data, jqXHR, textStatus) {
...
})
.fail(function(data, jqXHR, textStatus) {
...
})

closeLoading();
});

Promiseを使ったサンプルアプリケーション

promiseを使って作成したかんたんなアプリケーションです。

promiseを使った場合の非同期処理がどのように実行されるのか、視覚的にわかりやすいようにしました。

緑色ボタン: 記載された時間で処理が終了するタスクをストックします。
赤色ボタン: ストックしたタスクを実行します。
      (左から、タスクの順次処理、Promise.all()、Promise.race()での処理になっています。)
青色ボタン: アプリケーションとストックされたタスクを初期化します。

ぜひ遊んでみてください!

Next Task Starts.


init-application
300msec
500msec
1000msec
2000msec
dispatch-sequentially
dispatch-all
dispatch-race



まとめ

  • Promiseオブジェクトは"pending", "fulfilled", "rejected"の3つのステータスを持つ。
  • Promiseオブジェクトを使った非同期処理関数ではpromiseオブジェクトをreturnするようにする。
  • 非同期処理関数からreturnされたPromiseの状態によって、成功時・失敗時の処理を書き分ける。
  • 「async/await」を使ってうことで、非同期処理を同期的に実行できる。

非同期処理が思ったとおりの順番で実行されないと、ストレスがたまりますよね。

そんなあなたにぴったりの解決法はPromiseかもしれません。

ぜひPromiseを使った非同期処理をマスターして、煩雑な非同期処理コードとはおさらばしましょう!

前へ

【Git】Gitを知ろう! 昔のソース管理とGit

次へ

Laravelのwith関数(Eagerロード)でクエリチューニング