メインコンテンツまでスキップ

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関数内のみでしか使用できないため、このような記述をよく見かけます。

注記

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

課題

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

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