JavaScriptの練習をしてみた【非同期処理(Promise,async,await)編】

JavaScript

非同期処理は,おそらくJavaScriptで一番理解しずらい機能・文法です.

この記事では,JavaScriptにおける非同期処理の使い方について学習します.

初めに基本的な非同期処理について説明した後,非同期処理で例外処理を扱う方法としてPromiseを紹介し,さらにPromiseをきれいに書く方法としてasync,awaitを紹介します.

非同期処理とは

同期処理は,ソースコードの頭から順番に実行していく処理のことで,特に同期・非同期を意識せずに書かれたソースコードは同期的にされると思います.

例えば,以下のようなソースコードではA→B→Cの順番で処理され,合計で12秒の時間がかかります.

// 同期処理の例

// 1.同期処理A(1秒)

// 2.同期処理B(10秒)

// 3.同期処理C(1秒)

しかしながら,処理Bが10秒かかるので,先にCを処理してしまいたい場合があります.もしくは,処理Bがどれだけ時間がかかるか分からない場合にも,先にCを処理したいことはありそうですよね.

それに対して,非同期処理とは,終わらない処理を待たずに次の処理(ソースコード)に移るようなことを言います.

例えば,先ほどの処理Bが非同期であった場合,処理Bの終了を待たずにCが実行され,処理が終了する順番はA→C→Bとなります.

// 非同期処理の例

// 1.同期処理A(1秒)

// 2.非同期処理B(10秒)

// 3.同期処理C(1秒)

ここでは,以下のような機能を持つソースコードを作っていくことにします.

// 1.同期処理A(ほぼ0秒)
// "First"とコンソールに出力する処理

// 2.非同期処理B(1秒)
// 1秒後に"Third"とコンソールに出力する処理

// 3.同期処理C(ほぼ0秒)
// "Second"とコンソールに出力する処理

これを実行したときに期待される結果は以下の通りです.

First
Second
Third

このような操作を実現する非同期処理のソースコードをどのように書くのか,解説していきます.

同期処理の例

非同期処理を書いていく前に,同期処理のソースコードを作ってみます.

以下は,”First”と出力したあと,1秒後に”Third”と出力するsleep関数が実行され,”Second”と出力するソースコードです.

sleep関数の中身は気にせず,指定した秒数後に”Third”と出力する関数であると理解してもらえば大丈夫です.

// 指定ミリ秒後に,Thirdと出力する
function sleep(waitMsec) {
    var startMsec = new Date();
    // 指定ミリ秒間だけループさせる
    while (new Date() - startMsec < waitMsec);

    console.log("Third")
}

// "First"と出力する
console.log("First");

// 1秒後に"Third"と出力する
sleep(1000);

// "Second"と出力する
console.log("Second");

このソースコードを実行すると,以下が出力されます.

出力結果

First
Third
Second

まあ,こうなりますよね.

上から順番に同期的に処理していくので,”Third”が出力されるまで”Second”は出力されません.

それでは,非同期処理のソースコードを書いていきましょう.

非同期処理:コールバック関数

一番単純な方法で,非同期処理を実現したいと思います.

非同期処理関数『setTimeout』

setTimeoutは,

setTimeout(コールバック関数, 何秒後にコールバック関数を実行したいか[ms]);

により,コールバック関数を指定した時間の後に実行することができる,非同期処理です.

例えば,

setTimeout(() => {console.log("1000ms後にこれが表示される");}, 1000);

で,1秒後に文字列を出力することができます.

これを使って,”First”,”Second”,”Third”を表示されるソースコードを以下のように書いてみたとしましょう.

// "First"と出力する
console.log("First");

// 1秒後に"Third"と出力する
setTimeout(() => {console.log("Third");}, 1000);

// "Second"と出力する
console.log("Second");

結果は,以下のようになります.

First
Second
Third

きちんと,意図した非同期処理が行われていますね.

これでもOKなのですが,JavaScriptでは,非同期処理を記述する方法としてPromiseがあり,Promiseを使うことで例外処理を取り込むことができるので,この記事ではPromiseを使った形で書いていきます.

逆に言えば,例外処理を全く考慮しないのであれば,Promiseを使わなくても良いことになります.

ちなみにasync,awaitはPromiseを土台とした文法なので,これらを理解するにはまずPromiseの理解が必要です.

Promiseで非同期処理

Promiseは,非同期処理で例外処理を扱うために便利な構文です.

Promiseは,その非同期処理が「未処理」,「完了」,「失敗」のいづれかの状態であるか,およびその値を扱うことができます.

Promiseを使うには,Promiseオブジェクトからインスタンスを生成します.

Promiseインスタンス生成

Promiseインスタンスは,以下のように生成します.

// Promiseインスタンスを作成
const promise = new Promise((resolve, reject) => {
    // 非同期の処理が成功したときはresolve()を呼ぶ
    // 非同期の処理が失敗したときにはreject()を呼ぶ
});

このように,非同期処理をPromiseオブジェクトのコールバック関数として書きます.

Promiseのコールバック関数は,処理が無事に完了したときはresolve(戻り値),失敗したときはreject(エラー)を呼び,値を返します.

Promiseインスタンスは最初は「未処理」の状態になっており,処理が実行されると,完了もしくは失敗の状態になります.

今回は,いったんrejectを返す場合を無視して,処理はresolveのみを返すことにします.

// Promiseインスタンスを作成
const promise = new Promise((resolve) => {
    // 非同期処理
});

それでは,setTimeoutを非同期で処理したい内容として,Promiseに組み込んでみましょう.

// Promiseインスタンスを作成
const promise = new Promise((resolve) => {
    // 非同期で処理したい内容
    setTimeout(() => {
            console.log("1000ms後に出力される文字列");
            resolve();
        }, 1000);
});

今回は,文字列を表示させた段階でPromiseの状態をresolveにします.今回の場合は特に戻り値は設けないので,resolve();です.戻り値が必要な場合は,resolve(戻り値);とします.

あとは,このインスタンスpromiseを戻り値に持つ関数delayを定義しておきます.promiseは省略して,まとめて書いています.

function delay(timeoutMs) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("Third");
            resolve();
        }, timeoutMs);
    });
}

これで,Promiseを返す関数delayができました.

このPromiseの非同期処理の内容は「timeoutMs[ms]後に”Third”と返す」で,戻り値は特になし(resolve())です.

つまり,delayを実行すれば非同期処理が開始され,非同期処理が終わらずともdelayの次以降のコードに処理が進みます.

では,再度見てみましょう.

/**
 * Promiseを返す関数
 * 非同期処理の内容は,「1秒後に"Third"と出力する」
 */
function delay(timeoutMs) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("Third");
            resolve();
        }, timeoutMs);
    });
}

// "First"と出力する
console.log("First");

// 1秒後に"Third"と出力する.非同期処理なので,すぐに次に行く.
delay(1000);

// "Second"と出力する
console.log("Second");

出力結果

First
Second
Third

最初の方は関数の定義なので,本質的な処理は15行目から始まります.

まず,15行目が実行され,”First”がコンソールに表示されます.

次に,18行目のdelay(1000);が実行されます.この戻り値はPromiseインスタンスなので,非同期処理が開始されます.したがって,即座に次の行(19行目)以降に処理が移ります.

delay(1000);が実行されていますが,これは1秒後に”Third”を出力するものなので,その前に21行目が先に処理され,”Second”が出力されます.

最後に,非同期処理の内容が約1秒後に終わり,”Third”が表示されます.

Promiseで非同期処理の完了を確認する

このままでも良いのですが,非同期処理がちゃんと終わったことを確認したい,もしくは非同期処理が終わってから何かの処理をしたい場合があります.

そんなときは,Promiseの「then」メソッドを使います.thenメソッドは,処理が成功したとき(resolveが返ってきたとき)に評価されるメソッドです.

// delay(1000)は1秒後に"Third"と出力する非同期処理
delay(1000).then(() => {
    console.log("非同期処理終了(1秒後に呼ばれる)"); // 処理が成功したときに実行される
});

先ほどのソースコードに組み込むと,以下のようになります.

/**
 * Promiseを返す関数
 * 非同期処理の内容は,「1秒後に"Third"と出力する」
 */
 function delay(timeoutMs) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("Third");
            resolve();
        }, timeoutMs);
    });
}

// "First"と出力する
console.log("First");

// 1秒後に"Third"と出力する
delay(1000).then(() => {
    console.log("非同期処理終了(1秒後に呼ばれる)");
});

// "Second"と出力する
console.log("Second");

出力結果

First
Second
Third
非同期処理終了(1秒後に呼ばれる)

Promiseだけで非同期処理

ところで,実はthenの後の処理も非同期処理です.次の例を見てみましょう.

今度は,delay関数が返すPromiseのコールバック関数はsetTimeoutではなく,単純にresolveを返してconsole.logで文字列を出力するだけです.

function delay(timeoutMs) {
    return new Promise((resolve, reject) => {
        resolve();
        console.log("Promiseの処理");
    });
}

// "First"と出力する
console.log("First");

// 1秒後に"Third"と出力する
delay(1000).then(() => {
    console.log("非同期処理");
});

// "Second"と出力する
console.log("Second");

出力結果

First
何かしらの処理
Second
非同期処理

まず,”First”が出力されます.

次に,delay(1000)が呼び出され,Promiseのコールバック関数が実行されます.resolveを返し,”何かしらの処理”が出力されます.

直感的には次に出力されるのは”非同期処理”な気がしますが,次に出力されるのは”Second”です.つまり,thenの後のconsole.log(“非同期処理”)は非同期処理です.

その証拠に,以下を実行してみます.sleep(1000)は一秒後に”Third”と出力します.

// 指定ミリ秒後に,Thirdと出力する
function sleep(waitMsec) {
    var startMsec = new Date();
    // 指定ミリ秒間だけループさせる
    while (new Date() - startMsec < waitMsec);

    console.log("Third");
}

// "First"と出力する
console.log("First");

// thenのコールバック関数は非同期処理
delay(1000).then(() => {
    sleep(1000); //1s後に"Third"と出力
    console.log("非同期処理");
});

// "Second"と出力する
console.log("Second");

出力結果

First
Promiseの処理
Second
Third
非同期処理

“Third”と”非同期処理”が”Second”の後に出ていることが分かると思います.

つまり,Promiseだけで非同期処理を実現することができます.

Promiseで例外処理を見る

次に,Promiseでエラーが起きた場合を考えます.

コールバック関数内でエラーが起きたとして,そのPromiseを返す関数を作ります.

rejectで,Promiseは失敗を返します.戻り値は,エラーインスタンスnew Errorとします.

function delay(timeoutMs) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error("エラー発生")); //この時点でPromiseは失敗を返す
            console.log("何かしらの処理"); // この行は実行されない
        }, timeoutMs);
    });
}

Promiseがresolve(成功)を返したときはthenで次の処理を実行できましたが,reject(失敗)を返したときはcatchで次の処理に移ります.

catch内のコールバック関数の引数は,rejectの戻り値(この場合,”エラー発生”)になります.

// "First"と出力する
console.log("First");

// Promiseがrejectを返した場合はcatchで対応する
delay(1000).catch((error) => {
    console.log(error.message);
});

// "Second"と出力する
console.log("Second");

出力結果

First
Second
エラー発生

throwで例外を投げる

Promiseのコールバック関数内でthrowを投げると,その時点でrejectなPromiseを返し,その後の処理は実行されません.

また,thenメソッドはrejectが返ったときには実行されず,catchメソッドだけが実行されます.

function delay(timeoutMs) {
    return new Promise((resolve, reject) => {
        throw new Error("エラー"); // この時点でreject状態になって返す
        console.log("Promiseの処理"); // これは実行されない
    });
}

// "First"と出力する
console.log("First");

delay(1000).then(() => {
    console.log("成功"); // Promiseはrejectで返るため,これは実行されない
}).catch((error) => {
    console.log(error.message);
});

// "Second"と出力する
console.log("Second");

出力結果

First
Second
エラー

Promiseで非同期処理を複数回,順番に行いたい場合はthenメソッドを連続で繋げます.

function delay(timeoutMs) {
    return new Promise((resolve, reject) => {
        resolve(); // Promiseは成功状態になる
        console.log("Promiseの処理");
    });
}

// "First"と出力する
console.log("First");

delay(1000).then(() => {
    console.log("非同期処理1"); //
}).then(() => {
    console.log("非同期処理2"); //
}).catch((error) => {
    console.log(error.message);
});

// "Second"と出力する
console.log("Second");

出力結果

First
Promiseの処理
Second       
非同期処理1  
非同期処理2  

asyncとawait

asyncとawaitを使うことで,Promiseを見やすく書くことができます.

async Function

async Functionは,普通の関数もしくは関数式の前にasyncを付けることで定義できます.

async関数をどういうときに使うのかというと,Promiseを返したい関数を使いたいときや,次に紹介するawaitを使いたいときに定義します.

  • async Functionは必ずPromiseインスタンスを返す
  • async Function内ではawait式が利用できる

今まではPromiseのコールバック関数内でresolve,rejectを返していましたが,async Functionでは通常の関数のように値をreturnする形で書くことができます.

async function resolveFn() {
    return 5;
}

console.log("First");

resolveFn().then(value => {
    console.log(value); // => 5が返る.非同期処理
});

console.log("Second");

出力結果

First
Second
5

先ほど説明したPromise.thenメソッドを同様,thenの後(コールバック関数)の処理が非同期処理になっていますね.

これだけでも,Promiseインスタンスを使う場合より見やすくなったと思います.

例外を投げる場合は,Promiseの場合と同じように,以下のようにします.

async function exceptionFn() {
    throw new Error("例外が発生しました");
    console.log("この行は実行されない")
}

console.log("First");

exceptionFn().catch(error => {
    console.log(error.message); // => "例外が発生しました"
});

console.log("Second");

出力結果

First
Second
例外が発生しました

await

awaitを使うことで,非同期処理の流れをすっきりと書くことができます.

  • await式は,先ほど説明したasync Functionの中でのみ使用できる.
  • await式は右辺のPromiseインスタンスがresolve(完了)またはrejected(失敗)になるまでその場で非同期処理の完了を待つ

以下のPromiseを返す関数promise_funcは,引数のmessageを戻り値として,resolveを返します.asyncMainの中では,promise_funcをawaitで実行します.

/**
 * Promiseを返す関数
 */
 function promise_func(message) {
    return new Promise((resolve, reject) => {
        resolve(message); // Promiseは成功状態になる
    });
}

async function asyncMain() {
    const value1 = await promise_fanc("1回目"); // 非同期処理
    // 次の行はdoAsyncの非同期処理が完了されるまで実行されない
    console.log(value1); // => 1回目
    
    const value2 = await promise_fanc("2回目"); // 非同期処理
    // 次の行はdoAsyncの非同期処理が完了されるまで実行されない
    console.log(value2); // => 2回目
}
asyncMain();

出力結果

1回目
2回目

thenを使って順番に非同期処理すると複雑な見た目になりがちですが,このように,awaitを使うことですっきり書けます.

まとめ

・非同期処理とは,終わらない処理を待たずに次の処理(ソースコード)に移るような処理.

・Promiseは,その非同期処理が「未処理」,「完了」,「失敗」のいづれかの状態であるかと戻り値を返す.

・async Functionは必ずPromiseインスタンスを返し,async Function内ではawait式が利用できる.

・await式は右辺のPromiseインスタンスがresolve(完了)またはrejected(失敗)になるまでその場で非同期処理の完了を待つ.

参考:

非同期処理:コールバック/Promise/Async Function · JavaScript Primer #jsprimer
JavaScriptにおける非同期処理についてを紹介します。同期処理と非同期処理の違いやなぜ非同期処理が重要になるかを紹介します。非同期処理を行う方法としてコールバックスタイル、Promise、Async Functionを紹介します。

コメント

タイトルとURLをコピーしました