【Javascript】 非同期処理

Javascriptでは、Promiseasyncawaitを利用した非同期処理が可能です。しばしばHTTP通信に使われます。

ここで注意点として、非同期処理と並列処理は異なります。非同期な処理は、特定の手が空くタイミングで処理するというものです。

非同期処理が使われている例

例えば、http通信はよくあるJavascriptの非同期処理の例です。

import axios from "axios";
axios.get("https://example.com").then(res => {
    console.log(res.data);
});
console.log("foobar");
foobar
(https://example.com のHTML)

このプログラムは、https://example.comのHTMLを取得するプログラムです。axios.getの戻り値はPromiseのオブジェクトなので、.then(data => {...})で結果を取得しています。

実行結果を見ると、HTMLの取得より次の行のfoobarが先に実行されています。これが非同期処理の一例です。

このプログラムを最小構成で動かすには、Node.jsを導入し、npm i axiosをしてからNode.jsで動作させる必要があります。

また、非同期処理は特定のタイミングで実行されるので、処理によってはなかなか非同期処理が始まらない場合もあります。

特定のタイミングというのは、Javascriptでは各関数またはグローバルスコープでの実行終了後です。

const axios = require("axios").default;

// 下のfor文が終わらないと始まらない
axios.get("https://example.com").then(res => {
    console.log(res.data);
});

for(let i = 0; i < 5000000000;i++){
    if (i % 500000000 == 0) {
        console.log("hgoe");
    }
}

Promise

async, awaitの説明の前に、まずはPromiseを理解するべきです。

Promiseは、非同期処理をして結果を返すものです。と言われても想像がしづらいので例を見て考えていきましょう。

Promise成功時の処理

function f() {
    return new Promise((resolve, reject) => {
        resolve(1);
    });
}

// Promiseのインスタンスのthenメソッドを使う
f().then(data => {
    console.log(data);
});
1

ポイントはnew Promiseをしているところです。Promiseの引数には関数を入れ、その関数の引数はresolverejectです。

hoge().fuga().hige()のような書き方をメソッドチェーンと呼びます。

resolveは関数で、Promise内でresolveを呼ぶとPromiseの処理は成功したということになります。逆に、rejectを呼ぶ、または例外が排出されると失敗したということになります。

成功したデータは、Promiseのインスタンスに.then(data => {...})とすると取れます。これは上の例の通りです。上の例では、そのインスタンスがfの戻り値なので、f().then(...)としています。

Promise失敗時の処理

rejectの例もみてみましょう。

function f() {
    return new Promise((resolve, reject) => {
        reject("err");
    });
}

// Promiseのインスタンスのcatchメソッドを使う
f().catch(err => {
    console.error(err);
});
err

rejectを呼ぶとPromiseは失敗扱いになり、その時はcatchが呼ばれます。

次に、thencatchをつなげたパターンを見てみましょう。

let x = 0;
function asyncf(resolve, reject) {
    if (x == 0) {
        resolve("x is 0");
    }
    else {
        reject("Err!! x is not 0");
    }
}

function f() {
    return new Promise(asyncf);
}

// Promiseのインスタンスのthenとcatch両方を使う
f().then(data => {
    console.log(data);
}).catch(err => {
    console.error(err);
});
x is 0

この場合、Promiseが成功すればthenが、失敗すればcatchが呼ばれます。

catchを書かなかった状態で失敗した場合

例外がスローされます。

function f() {
    return new Promise((resolve, reject) => {
        reject("err");
    });
}

// Promiseのインスタンスのthenを使う
f().then(data => {
    console.log(data);
}); // Uncaught (in promise) rejected

例外に対応するため、thencatchは必ず両方書きましょう。

try-catchでこれに対応することはできません。.then.catchが呼び出されるのは、非同期処理が走るタイミングであり、その時点ではもうtryブロックの処理が終わっているためです。

finally

thencatch以外にfinallyというものもあります。これは、Promiseの成功失敗に関わらず実行されるものです。

let x = 0;
function asyncf(resolve, reject) {
    if (x == 0) {
        resolve("x is 0");
    }
    else {
        reject("x is not 0");
    }
}

function f() {
    return new Promise(asyncf);
}

// Promiseのインスタンスのthenとcatch両方を使う
f().then(data => {
    console.log(data);
}).catch(err => {
    console.error(err);
}).finally(() => {
    console.log("finally実行");
});
x is 0
finally実行

通信中のフラグを戻すなど、使い所はいくつかあります。

Promiseは待てない

Promiseしたものは非同期で実行されますが、場面によっては同期的に動いてほしい場面が多くあります。例えば、Web上のAPIをたたき、結果を取得してから先の処理を進めたい場合などです。

const axios = require("axios");

let data = null;
axios.get("https://example.com").then(res => {
    data = res.data;
});

// ここにdataの処理を書きたい!

1つは、この後紹介するasync, awaitを利用する方法です。もう一つは、メソッドを重ねることです。

const axios = require("axios");

let data = null;
axios.get("https://example.com").then(res => {
    dataProcess(res.data);
});

function dataProcess(data) {
    // ...
}

しかし、この方法はメソッドの階層が深くなる、過剰にメソッドが増えるなどの問題があります。

resolveやrejectを呼ぶタイミング

これを呼ぶのは、Promise内に入れた関数の処理が走りきってからでも問題ありません。つまり、setTimeoutなどで後から呼び出しても良いというわけです。

function f() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("1秒後にresolve");
        }, 1000);
    });
}

console.time("await time");
f().then(data => {
    console.log(data);
    console.timeEnd("await time");
});
1秒後にresolve
await time: 1001.1640625 ms

結果の通り、1秒経ってから.thenが実行されていることが分かります。

asyncとawait

Promiseの記法を省略したのがasyncです。

// new Promiseの代わりに, 関数の頭にasyncを付ける
async function f() {
    return "resolved";
}

// ここは普通に呼び出す場合は変化なし
f().then(data => {
    console.log(data);
}).catch(err => {

});

console.log("foobar");
foobar
resolved

functionや、() => {...}の前にasyncを付けると、その関数は非同期処理を実行することができます。

クラスのメソッドにもasyncを付けることができます。同じようにメソッド名の前にasyncを付けます。

このプログラムは, 直前の節で紹介したプログラムと同等です。つまり、

async function f() {
    return "resolved";
}
function f() {
    return new Promise((resolve, reject) => {
        resolve("resolved");
    });
}

の2つは同等です。

行を増やしたパターンを考えると、

async function f() {
    for (let i = 0; i < 10; i++) {
        // ...
    }
    return "resolved";
}
function f() {
    return new Promise((resolve, reject) => {
        for (let i = 0; i < 10; i++) {
            // ...
        }
        resolve("resolved");
    });
}

の2つが同等になります。

await

awaitは、asyncの処理が終わるのを待つというものです。つまり、同期的に実行できます。

async function f() {
    return "resolved";
}

async function f2() {
    const res = await f();
    console.log(res);
    console.log("foobar");
}

f2();
resolved
foobar

f2内に処理を入れたのは、まだトップレベルでawaitできないためです。

次期ECMAScriptではこれが可能になる可能性があります。

注意点として、awaitasyncな関数内でしか利用できません。非常に重要なので今すぐ覚えておきましょう。

この実行結果とコードから、asyncな関数をawaitを付けて呼び出すと、あたかも同期処理のように実行できる事がわかります。

await時のcatch

awaitをつけると同期実行されるので、thenなどが不要になります。このとき、rejectされるとどうなるかというと、例外が排出されます。

function f() {
    return new Promise((resolve, reject) => {
        reject("rejected")
    });
}

async function f2() {
    try {
        const res = await f();
        console.log(res); // 実行される前にthrowされる
    } catch (err) {
        console.error(err);
    }
}

f2();
rejected

つまり、awaitを使えば.then(data => {...})dataは普通の戻り値に、.catch(err => {...})は普通の例外処理に、ということです。

async内でrejectする

お察しの方はいるかもしれませんが、asyncの関数ではresolverejectは使えません。

asyncな関数内でreturnすると、それはresolveと同等になるということはこれまでのプログラムからわかりますが、rejectするにはどうすればよいでしょうか。

これは、例外をthrowすればよいです。

async function f() {
    throw "rejected"; // throwは, Promise内でrejectを呼んだのと同等
}

async function f2() {
    try {
        let res = await f();
    } catch (err) {
        console.error(err);
    }
}

f2();
rejected

これで、async/awaitとPromiseの置き換えは把握できたのではないでしょうか。

まとめ

まず、非同期処理と並列実行は異なります。

Promiseでは、指定したコードが特定のタイミングで自動で動作し、その動作結果をPromiseのインスタンスに.then.catchをつなげて処理します。

asyncawaitは、それを短縮し、awaitを利用すれば同期的な処理で書けるようになります。

JavascriptではしばしばAPI通信などをするので、非同期処理の動きは知っておくと必ず役に立ちます。

async thumb

役に立ったらシェアしよう!