非同期処理は快適なUI / UXを実現する上で重要な概念です。

例えば、ファイルアップロードなどのデータの取り扱いにおいて一定の待ち時間が発生する処理を同期処理で行った場合、ユーザーはそれが完了するまでローディングを眺めるしかありません。

1つの画像を読み込むのに数秒かかり、それが終わったら次の画像にまた数秒となれば、ユーザーはアプリケーションの利用にうんざりして離脱するでしょう。

ですが仮に、複数の画像やデータ読み込みを並行して行えるとどうでしょう?ユーザーの待ち時間は圧倒的に削減されます。非同期処理は、あらゆる処理を並行して行うための重要な概念です。

今回はその重要な概念である非同期処理と、ES2015より導入されたPromiseインスタンスという仕組みについて解説します。

処理解説

プログラミング言語の処理は大きく同期処理・非同期処理の2つに分類されます。

同期処理

同期処理ではソースコードを上から順番に実行し、一つの処理が終わるまで次の処理は実行されません。そのような特徴から、同期処理はシングルスレッドとも呼ばれます。

JavaScriptは基本的にブラウザのメインスレッドで実行されるため、処理の完了までに時間がかかる場合には、ユーザーにとっては大きなストレスになります。

メインスレッドとは、ブラウザーがユーザーのイベントや描画を処理するところです。メインスレッドで動作させる必要がある処理が少ないほど、メインスレッドがユーザーのイベントに応答したり、描画したりすることが増え、ユーザーへの反応が全般的に良くなります。

メインスレッドは表示の更新などのUI全般に関する処理が行われているので、JavaScriptの他の処理で占有されることで、画面が表示されない・フリーズするといった不具合に繋がることがあります。

非同期処理

非同期処理はある処理を実行している最中に、その処理を止めることなく他の処理を実行できる方式です。Aajx(Asynchronous JavaScript and XML)と呼ばれます。

非同期処理をうまく活用することで、全体の処理速度を早められるメリットがある一方で、現在どの処理を実行しているかの把握が難しくなるというデメリットもあります。

非同期処理はメインスレッドで実行される

上述、「メインスレッドでの処理の多さは遅延につながる」と説明しましたが、非同期処理の実行場所も基本的にはメインスレッドです。JavaScriptでは一定の例外を除き非同期処理は並行処理として扱われます。

並行処理は、処理を一定の単位ごとに分割して処理を切り替えながら実行することです。そのため非同期処理の中に非常に重たい処理がある場合は、非同期処理の切り替えに遅延が発生する仕組みになっています。

Promiseとは

Promise は非同期処理を抽象化することにより、コーディングをシンプルにわかりやすく見せる処理です。

コールバック

コールバックとは非同期処理として実行が完了した際に実行する別の処理を指します。下記のサンプルコードは非同期処理をコールバックでつなげて記述したものです。やりたいことは明確ですがネストが深すぎて処理の把握が難しそうな雰囲気が漂っています。

method1(function(data1) {
  method2(function(data2) {
   method3(function(data3) {
    method4(function(data4) {
    //処理
    })
   })
  })
})

非同期処理とコールバックは組み合わせとしてよく使用されますが、複雑化しやすいという欠点があります。単純に1つ or 2つの処理だけなら問題ありませんが大抵はサーバーから受信したデータを適切な形式に加工したり、配列操作などの処理が発生するので処理は連続します。

その場合はコールバック関数のネストが深くなることに加えて、エラー処理も記述する必要があるのでコードの可読性は著しく下がるという問題点がありました。

このコールバック地獄を避けるために考えられた仕組みがPromiseです。

Promiseチェーンを用いた処理の記述方法

Promiseは非同期処理における統一的なインターフェイスを提供するビルトインオブジェクトです。Promiseによる統一的な処理方法は、複数の非同期処理を扱う場合に特に効力を発揮します。時に上記のようなコールバックのネスト地獄を解決することができます。

上記の例でも「非同期処理が終了したら次の非同期処理」という流れは同じですが、これから紹介するPromiseではこのような複数の一連の非同期処理を簡単に表現できる記述方があります。

この仕組みは、処理をthen(), catch()でつないで、常に新しいPromiseインスタンスを作成して処理結果を返します。

Promise.resolve(value)
.then((value1) => {
    //処理
    return value2;
})
.then((value2) => {
    //処理
    return value3;
})
.catch((error) => {
   // 処理
})

Promiseでは、thenメソッドの戻り値であるPromiseインスタンスにさらにthenメソッドで処理を登録できます。この仕組みをメソッドチェーンと呼びます。

Promiseチェーンでは、Promiseが失敗しない限り順番にthenメソッドで登録したコールバック関数を実行します。

catch()メソッドでは処理に失敗したときの処理を書きます。

つまり、処理が成功した場合はthen()メソッドで登録された成功時の処理のみが呼び出され失敗したときは成功時の処理は呼び出されずに、catchメソッドで登録した処理のみが実行されます。

処理結果の値を次の処理に渡す

Promiseチェーンでは、コールバック関数で返した値を次の処理に引数として渡すことができます。then()やcatchメソッドのコールバック関数は数値、文字列、オブジェクトなどの任意の値を返すことができます。

エラーハンドリングについて

エラーハンドリングとは、プログラムの処理中に処理を妨げるエラーが発生した際に、その処理をエラーとして対処する処理のことです。これらは通常「例外処理」とも呼ばれます。

エラーハンドリングの処理内容は、プログラムを実装する際に予め用意しておく必要があります。エラーハンドリングの処理を実装せずに正常処理のみを記述したサイトやシステムでは、エラーが発生した際にどのような挙動になるかが予測できないことや、どこの処理に原因があるかの把握が難しくなります。

そのためエラーハンドリングの記述はプログラミングを行う上では必須の作業です。

Promiseでのエラーハンドリング

Promiseにはエラーハンドリングを記述するための仕組みも用意されています。

const promise = new Promise(function(resolve, reject)) {
 //処理
 if(条件) {
   resolve(成功)
 } else {
   reject(失敗);
 }
}

Promiseが正常に実行された場合は、resolveを使用していましたがエラー処理の場合はrejectを使用します。

Promiseを使用して非同期処理を実行する場合に、基本的には第一引数のresolveのみを記述しますが、第2引数にreject()のPromiseオブジェクトが返る仕組みになっています。

そのため、下記のcatch()での例外処理の引数にerrorが渡されていますが、これはPromiseの非同期の戻り値にreject()が含まれているからです。

Promise.resolve(value)
.then((value1) => {
    //処理
    return value2;
})
.then((value2) => {
    //処理
    return value3;
})
.catch((error) => {
   // 処理
})

今回は

・同期処理・非同期処理の仕組みと違い
・非同期処理に使用されるPromiseの概念
・Promiseでのエラーハンドリングの仕組み

について解説をしました。非同期処理については今後の記事でも多く使用していきますので理解を深めていってください。

参考

Promise : https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Using_promises

カテゴリー: JavaScript