JavaScriptの非同期処理

同期処理と非同期処理

時間の掛かる処理があります。例えば、インターネット通信や、ファイルの読み込みといった類の処理です。これらの処理を一般的な方法で実装しようとすると、その処理を行う間、ユーザーからの入力が受けられない状態になってしまいます。これが俗に言う、フリーズした状態です。

幸いなことに、JavaScriptでは、時間の掛かる処理の待ち時間を、他の処理のために充てることができるような仕組みが用意されています。これを、非同期処理といいます。JavaScriptにおける非同期処理は、歴史的にはコールバックを用いて処理されます。

setTimeout(() => {
console.log('Text 1');
}, 1000);
console.log('Text 2');

setTimeout関数は、2つの引数をとり、第1引数で指定された関数を、第2引数で指定された時間(ミリ秒)後に実行します。このように、システムによって処理の完了後に呼び出される関数を、コールバック関数と呼びます。上記の例では、コールバック関数の実行は後回しにされ、非同期的に実行されます。このため、実行結果はText 2Text 1の順となります。

それでは、setTimeout関数を用いて、1秒毎に異なるメッセージを表示させることを考えてみましょう。すぐに思いつくのは、以下のようなコードです。

setTimeout(() => {
document.write('Text 1');
setTimeout(() => {
document.write('Text 2');
setTimeout(() => {
document.write('Text 3');
setTimeout(() => {
document.write('Text 4');
}, 1000);
}, 1000);
}, 1000);
}, 1000);

くどいですね。一昔前のJavaScriptでは、上記コードのように、コールバック関数が大量に使用された結果、インデントが非常に深くなるという事態に陥っていました(コールバック地獄)。

この状況を解決するために生まれたのが、Promiseと呼ばれる考え方です。Promiseを用いると、上記のコードは以下のように書き換えることができます。

function mySetTimeout(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
});
}
mySetTimeout(1000).then((value) => {
document.write('Text 1');
return mySetTimeout(1000);
}).then((value) => {
document.write('Text 2');
return mySetTimeout(1000);
}).then((value) => {
document.write('Text 3');
return mySetTimeout(1000);
}).then((value) => {
document.write('Text 4');
return mySetTimeout(1000);
});

Promiseに対応する関数やメソッドの作成

setTimeout関数は、昔ながらのコールバック関数を用いた形式となっています。そのため、Promiseを用いてsetTimeoutを利用するためには、setTimeoutをPromiseに対応させるための関数(ラッパー関数)を作成する必要があります。

Promiseに対応する非同期処理関数は、Promiseクラスのインスタンスを返します。Promiseクラスのコンストラクタは、ただ1つの引数をとります。この引数はシステムによって呼び出される関数で、呼び出し可能な二つの引数(resolvereject)をとることができます。非同期処理はその関数の中で実行し、正常に終了した時点でresolveを、異常終了した場合はrejectを呼びます。

resolveはただ一つの引数を取り、非同期処理の結果を渡します。rejectはただ一つの引数を取り、処理の失敗の理由を渡します。

Promiseを用いた非同期処理に対応する関数の使い方

Promiseに対応する非同期処理関数により得られたPromiseクラスのインスタンスは、thenメソッドを持ちます。thenメソッドはただ一つの引数を取り、この引数は非同期処理の終了時にシステムによって呼び出される関数です。この関数はただ一つの引数を取り、非同期処理の結果を受け取ります。

また、この関数の中で別のPromiseを返すと、そのPromiseがresolveされた時点で、thenメソッドの返り値として得られるPromiseがresolveされます。

Async / Await文

Async / Await構文を用いると、Promiseを用いた非同期処理をさらに簡潔に記述することができます。

async function promiseTest() {
await mySetTimeout(1000);
document.write('Text 1');
await mySetTimeout(1000);
document.write('Text 2');
await mySetTimeout(1000);
document.write('Text 3');
await mySetTimeout(1000);
document.write('Text 4');
}
promiseTest();

asyncキーワードを指定した関数の中でawait文が実行されると、その関数はPromiseが完了(resolveまたはreject)されるまで実行が中断されます。Promiseがresolveされた場合にはその値がawait文の戻り値となります。

なお、await文を使用する関数には、asyncキーワードを付与する必要があります。asyncキーワードが付与された関数は、自動的に戻り値がPromise型となります。

await文のの戻り値を利用したサンプル

await文は、式の中で使用することもできます。この場合、式の評価が一時中断され、Promiseがresolveされてから続きが評価されます。

function delayedSquare(number) {
console.log(`delayedSquare ${number}`);
return new Promise((resolve) => {
setTimeout(() => {
resolve(number ** 2);
}, 1000);
});
}
async function delayedPythagoras(a, b) {
return Math.sqrt(await delayedSquare(a) + await delayedSquare(b));
}
(async () => {
console.log(await delayedPythagoras(3, 4));
})();

アロー関数にも、直前にasyncキーワードを付与することにより、async関数として扱うことができるようになります。上の例では、asyncキーワードをつけて生成した無名関数をその直後で実行させています。JavaScriptにおいて、awaitキーワードはasync関数内のみでしか使用できないため、このような記述をよく見かけます。

note

JavaScriptのこの制約をなくすための提案がなされており、いずれは実現されるものと思われます。

課題

input要素上でキーが押されるとresolveされるPromiseを返す関数getKeyを定義してください。以下のように使用できることを想定しています。

(async () => {
while (true) {
console.log(await getKey());
}
})();