やっやこと
JSの非同期について
非同期 JavaScript
非同期とは?
通常は、あるプログラムのコードは書かれた順に、一度にひとつのことだけが起こるように実行されます。もしある関数が別の関数の結果に依存するのであれば、その関数は他の関数の処理が完了して結果を返すまで待たなくてはならず、それまでは、ユーザー視点からはプログラム全体は止まっているのと本質的には同じです。
特に、マルチコアプロセッサーが利用できる時代においては。他のタスクを別のプロセッサーコアに処理させて、それが終わった時に知らせることができるのに、座って待っているのは意味がありません。このように合間に別の仕事を終わらせる、ということが非同期プログラミングの基本です。
ブロッキングコード
ウェブアプリがブラウザー上で高負荷なコードを実行すると、ブラウザーは固まって見えるかもしれません。これをブロッキングといいます。
原因は一般的に言えば JavaScript はシングルスレッドだから
スレッド
スレッドとは、基本的にプログラムがタスクを完了させるのに使用できる、単一のプロセスです。各スレッドは 1 度に 1 つのタスクを実行することしかできません。
Task A --> Task B --> Task C
各タスクは順次実行されます。すなわち、あるタスクが完了しなければ、その次のタスクは開始されません。
現在では多くのコンピューターは複数のコアを持つため、一度に複数のことをすることができます。マルチスレッドをサポートするプログラミング言語は、複数のコアを使用して同時に複数のタスクを完了させることができます。
Thread 1: Task A --> Task B
Thread 2: Task C --> Task D
従来より JavaScript はシングルスレッドです。複数のコアを利用しても、メインスレッドと呼ばれる単一のスレッド上でタスクを実行できるだけ
少し経ってから、JavaScript はこういった問題に役立つ、いくつかのツールを手に入れました。Web workers によって worker と呼ばれる別個のスレッドに JavaScript の処理の一部を移すことが可能となり、そのことで複数の JavaScript のコードを同時に実行することができるようになります。一般的に worker は、ユーザーの操作がブロックされないように、高コストな処理をメインスレッドとは別のところで実行するために使用されます。
Web worker はかなり便利ですが、制限もあります。
- Web worker は DOM にアクセスできません —— Worker に直接ユーザーインターフェイスを更新させるようなことはできません。
- worker によって実行されるコードはブロックしないとは言え、根本的には依然として同期的であるということ
このような問題を解決するために、ブラウザーを利用して特定の処理を非同期に実行することができます。Promises のような機能を利用することで、ある処理(例:サーバーからの画像の取得)を実行し、その結果が返ってくるまで別の処理の実行を待たせることができる
非同期JavaScript
2つのタイプあある
- 古いスタイルのコールバック
- 新しいpromiseスタイルのコード
非同期コールバック
非同期コールバックは、バックグラウンドでコードの実行を開始する関数を呼び出すときに引数として指定される関数
バックグラウンドコードの実行が終了すると、コールバック関数が呼び出され、作業が完了したことを通知したり、関心のあることが発生したことを通知したりします。
非同期処理:コールバック/Promise/Async Functionこいつをまとめる
非同期処理はメインスレッドで実行される
JavaScriptにおいて多くの非同期処理はメインスレッドで実行されます。
非同期処理は名前から考えるとメインスレッド以外で実行されるように見えますが、 基本的には非同期処理も同期処理と同じようにメインスレッドで実行されます。
JavaScriptでは一部の例外を除き非同期処理が並行処理(concurrent)として扱われます。 並行処理とは、処理を一定の単位ごとに分けて処理を切り替えながら実行することです。 そのため非同期処理の実行中にとても重たい処理があると、非同期処理の切り替えが遅れるという現象を引き起こします。
ただし、非同期処理の中にもメインスレッドとは別のスレッドで実行できるAPIが実行環境によっては存在します。 たとえばブラウザではWeb Worker APIを使い、メインスレッド以外でJavaScriptを実行できます。 このWeb Workerにおける非同期処理は並列処理(Parallel)です。 並列処理とは、排他的に複数の処理を同時に実行することです。
非同期処理のすべてをひとくくりにはできませんが、基本的な非同期処理(タイマーなど)はメインスレッドで実行されているという性質を知ることは大切です。JavaScriptの大部分の非同期処理は非同期的なタイミングで実行される処理であると理解しておく必要があります。
非同期処理と例外処理
同期処理では、try…catch構文を使うことで同期的に発生した例外がキャッチできます
try {
setTimeout(() => {
throw new Error("非同期的なエラー");
}, 10);
} catch (error) {
// 非同期エラーはキャッチできないため、この行は実行されません
}
console.log("この行は実行されます");
tryブロックはそのブロック内で発生した例外をキャッチする構文です。 しかし、setTimeout関数で登録されたコールバック関数が実際に実行されて例外を投げるのは、すべての同期処理が終わった後となります。 つまり、tryブロックで例外が発生しうるとマークした範囲外で例外が発生します。
そのため、setTimeout関数のコールバック関数における例外は、次のようにコールバック関数内で同期的なエラーとしてキャッチする必要があります。
// 非同期処理の外
setTimeout(() => {
// 非同期処理の中
try {
throw new Error("エラー");
} catch (error) {
console.log("エラーをキャッチできる");
}
}, 10);
console.log("この行は実行されます");
このようにコールバック関数内でエラーをキャッチできますが、非同期処理の外からは非同期処理の中で例外が発生したかがわかりません。 非同期処理の外から例外が起きたことを知るためには、非同期処理の中で例外が発生したことを非同期処理の外へ伝える方法が必要です。
Promise
エラーファーストコールバックは非同期処理を扱うコールバック関数の最初の引数にエラーオブジェクトを渡すというルールでした。 Promiseはこれを発展させたもので、単なるルールではなくオブジェクトという形にして非同期処理を統一的なインターフェースで扱うことを目的にしています。
Promiseインスタンスの作成
Promiseはnew演算子でPromiseのインスタンスを作成して利用します。 このときのコンストラクタにはresolveとrejectの2つの引数を取るexecutorと呼ばれる関数を渡します。 executor関数の中で非同期処理を行い、非同期処理が成功した場合はresolve関数を呼び、失敗した場合はreject関数を呼び出します。
const executor = (resolve, reject) => {
// 非同期の処理が成功したときはresolveを呼ぶ
// 非同期の処理が失敗したときはrejectを呼ぶ
};
const promise = new Promise(executor);
Promiseチェーンで一度キャッチすると、次に呼ばれるのは成功時の処理となります。 そのため、catchメソッドで返した値は次のthenメソッドのコールバック関数に引数として渡されます。
Promise.reject(new Error("失敗")).catch(error => {
// 一度catchすれば、次に呼ばれるのは成功時のコールバック
return 1;
}).then(value => {
console.log(value); // => 1
return value * 2;
}).then(value => {
console.log(value); // => 2
});
Promise.allで複数のPromiseをまとめる
Promise.allメソッドは Promiseインスタンスの配列を受け取り、新しいPromiseインスタンスを返します。 その配列のすべてのPromiseインスタンスがFulfilledとなった場合は、返り値のPromiseインスタンスもFulfilledとなります。 一方で、ひとつでもRejectedとなった場合は、返り値のPromiseインスタンスもRejectedとなります。
返り値のPromiseインスタンスにthenメソッドで登録したコールバック関数には、Promiseの結果をまとめた配列が渡されます。 このときの配列の要素の順番はPromise.allメソッドに渡した配列のPromiseの要素の順番と同じになります。
Promise.race
Promise.raceメソッドでは複数のPromiseを受け取りますが、Promiseが1つでも完了した(Settled状態となった)時点で次の処理を実行します。
Promise.raceメソッドはPromiseインスタンスの配列を受け取り、新しいPromiseインスタンスを返します。 この新しいPromiseインスタンスは、配列の中で一番最初にSettled状態となったPromiseインスタンスと同じ状態になります。
- 配列の中で一番最初にSettledとなったPromiseがFulfilledの場合は、新しいPromiseインスタンスもFulfilledになる
- 配列の中で一番最初にSettledとなったPromiseがRejectedの場合は、新しいPromiseインスタンスも Rejectedになる
Promise.raceメソッドを使うことでPromiseを使った非同期処理のタイムアウトが実装できます。 ここでのタイムアウトとは、一定時間経過しても処理が終わっていないならエラーとして扱う処理のことです。
Async Function
Async Functionは通常の関数とは異なり、必ずPromiseインスタンスを返す関数を定義する構文です。
重要なこととしてAsync FunctionはPromiseの上に作られた構文です。
Async FunctionはPromiseを返す
Async Functionとして定義した関数は必ずPromiseインスタンスを返します。 具体的にはAsync Functionが返す値は次の3つのケースが考えられます。
- Async Functionが値をreturnした場合、その返り値を持つFulfilledなPromiseを返す
- Async FunctionがPromiseをreturnした場合、その返り値のPromiseをそのまま返す
- Async Function内で例外が発生した場合は、そのエラーを持つRejectedなPromiseを返す
await文
await式は次の箇所で利用できる式です。 次の箇所以外ではawait式は構文エラーとなるため、利用できません。
- Async Functionの関数の直下
- ECMAScriptモジュールの直下
await式は、awaitの右辺(Promiseインスタンス)の評価結果を値として返します このawait式の評価方法は評価するPromiseの状態(FulfilledまたはRejected)によって異なります。
awaitの右辺のPromiseがFulfilledとなった場合は、resolveされた値がawait式の返り値となります。
await式の右辺のPromiseがRejectedとなった場合は、その場でエラーをthrowします。 またAsync Function内で発生した例外は自動的にキャッチされます。 そのためawait式でPromiseがRejectedとなった場合は、そのAsync FunctionがRejectedなPromiseを返すことになります。
Async Function内でawait式を使って処理を待っている間も、関数の外側では通常どおり処理が進みます。
このようにawait式でAsync Function内の非同期処理を一時停止しても、Async Function外の処理が停止するわけではありません。 Async Function外の処理も停止できてしまうと、JavaScriptでは基本的にメインスレッドで多くの処理をするため、UIを含めた他の処理が止まってしまいます。 これがawait式がAsync Functionの外で利用できない理由の1つです。