新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

JavaScriptのasync/awaitをもう少しちゃんと理解する

今までJavaScriptのasync/awaitを、理解が曖昧なまま何となく使っていて、うまく行かない時はPromiseを使ったりしていました。

しかし、最近はasync/awaitが使われているのをよく目にするようになってきたため、もう少しちゃんと理解しないといけないかなぁと思いはじめ、その理解メモです。

async/awaitの基本

まずは基本から。async/awaitを使う目的は、非同期処理を同期処理っぽく、順番に記述できるところでしょうか。

基本的な機能は下記のようなところです。

  • Promiseを返す関数をawaitで呼び出すと、次の処理には移らず、Promiseに格納した実値(resolve・rejectで返される値)が確定するまで待っていてくれる。
  • awaitは、PromiseからPromiseに格納した実値を取り出して返してくれる。
  • Promiseを返す関数がrejectした場合は、awaitは例外を返す。
  • awaitの例外はtry/catchのcatchで受ける。
  • awaitを使う関数はasync関数でなければいけない。

コードにすると下記のようになります。よく見るパターンです。

// Promiseを返す関数
function test_promise(v){
    return new Promise((resolve, reject)=>{
        if(v){
            resolve("OK");
        } else {
            reject("ERROR");
        }
    });
}

(async ()=>{
    try{
        const ret = await test_promise(true);
        console.log(ret);    // 「OK」と表示される
    }catch(err){
        console.log(err);    // 「ERROR」と表示される
    }
})();

async関数とは?

async関数とは要約すると「Promiseを返す関数の別の書き方」ってとこでしょうか。

  • return ~が、Promiseでいうところのresolve(~)に相当する。
  • throw ~が、Promiseでいうところのreject(~)に相当する。

例えば先程のPromiseを返す関数test_promise()をasyncで書き直すと下記のようになります。

async function test_async(v){
    if(v){
        return "OK";
    } else {
        throw "ERROR";
    }
}

async関数の注意点

じゃあPromiseを全てasync関数に置き換えらるかというと残念ながら無理です…。

async関数の中で非同期関数を呼び、その中からPromise・throwを返すことができません。

例えば

function test_promise(v){
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            if(v){
                resolve("OK");
            } else {
                reject("ERROR");
            }
        }, 1000);
    });
}

のようなPromiseを返す関数を

async function test_async(v){
    setTimeout(()=>{
        if(v){
            return "OK";    // test_async()の戻り値じゃない
        } else {
            throw "ERROR";
        }
    }, 1000);
}

と書いてもうまくいきません。

無理やりasyncでも成立するようにさせると下記のようになります。

async function test_async(v){
    try{
        const ret = await new Promise((resolve, reject)=>{
            setTimeout(()=>{
                if(v){
                    resolve("OK");
                } else {
                    reject("ERROR");
                }
            }, 1000);
        });
        return ret;
    }catch(err){
        throw err;
    }
}

もはや、Promiseでいいじゃんといった感じです。

async関数の中では、awaitを使い逐次処理していき、最後に明示的にreturn/throwで結果を返すのが、一番オーソドックスな使い方のようです。

asyncの中で使う関数全てがPromiseを返してくれるのだったなら、awaitだけでシンプルに書けるので、asyncを使うかPromiseを使うかはケースバイケースかなと思います。

Array.map()の中で非同期処理を行う

配列の各要素を使って非同期処理を行い、全ての処理が終わるまで待ってもらい、処理結果を配列で受け取るやり方です。このパターンはちょいちょい見かけます。

方法

  • mapから呼び出す関数をasyncにする。
  • async関数では実行結果をreturnで返す(async関数なので、returnで返された値はPromiseとなって返される)。
  • mapから生成された配列をPromise.allに格納する。
  • Promise.allをawaitで受ける。

コードにすると下記のようになります。

function test_promise(v){
    new Promise((resolve, reject)=>{
        setTimeout(()=>{
            resolve(v*10);
        }, 1000);
    });
}

(async ()=>{
    try{
        const array = [1, 2, 3];
        const ret = await Promise.all(
            array.map( async (v)=>{
                return test_promise(v);
        } ));
        console.log(ret);    // [10, 20, 30] と表示される
    }catch(err){
        console.log(err);
    }
})();

async/awaitがどう動作しているか見ていく前に、Promise.all()についておさらいを…

Promise.all()についておさらい

  • Promise.all()は、Promiseの配列を引数に取る。
  • 全てのPromiseが成功した場合は、then(val)で受けて、引数(val)は結果の配列となる。
  • 1つでも失敗した場合は、catch(err)で受けて、引数(err)は最初の失敗のreject(err)の値が入る。

以上を踏まえてコードを振り返ると…

  1. array.map()は各要素に対して非同期処理を実行し、その実行結果をPromiseとして返すので、array.map()はPromiseの配列となる。
  2. Promise.all()で上記のPromiseの配列を受ける。
  3. 全ての処理が成功した場合は、awaitがPromiseの配列を、結果の配列へと展開する
  4. 1つでも処理が失敗になった場合は、awaitで例外が投げられて、catchで受ける。

の一連の処理が行われ、結果の配列がawaitから返され、retへと代入されます。

asyncによりPromiseの配列ができるところと、Promiseの配列から次の処理に移らないようawaitで足止めし、かつ、Promiseから値に展開するために、配列をPromise.all()で受けるところがポイントですね。

Array.forEach()の中で非同期処理を行う

Array.map()がうまくいったので、Array.forEach()でもうまくいくかも!?と思いたいところですが、結論から言うとできないです。

例えば、

function test_promise(v){
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            console.log(v);
            resolve();
        }, 1000);
    });
}

(async ()=>{
    try{
        const array = [1, 2, 3];
        await array.forEach( async (v)=>{
            await test_promise(v);
        });
        console.log("END");
    } catch(err){
        console.log(err);
    }
}

の結果は

1
2
3
END

とはならず

END
1
2
3

になります。

意図した挙動にするには、Array.forEach()は諦めて、地道にfor/ofを使うしかないです。

(async ()=>{
    try{
        const array = [1, 2, 3];
        for(const v of array){
            await test_promise(v);
        }
        console.log("END");
    } catch(err){
        console.log(err);
    }
}

もしくは、前述のArray.map()を、Array.forEach()代わりに使ってもできますね。

(async ()=>{
    try{
        const array = [1, 2, 3];
        await Promise.all( array.map( async (v)=>{
            await test_promise(v);
            return;
        }));

        console.log("END");
    } catch(err){
        console.log(err);
    }
}

「await Promise.all(〜)」を抜けた時点で、「return」が完了、つまり、「async(v)=>{}」内の処理が完了しています。

しかも、非同期で実行されるので、1000msで全てが完了します。一方「for/of」の方は、1つ1つawaitで完了を待って実行するので、全て完了するのに3000msかかります。

ザックリですが、以上である程度async/awaitは使えるようになるのではないでしょうか。

感想

async/awaitを使えばPromiseは必要無くなるかと思ったのですが、そうはいかないようですね。

awaitで全体の見通しが良くなるのですが、並列処理できるところをついついawaitで逐次処理にしてしまって、速度が遅くなることもありますね。

関連カテゴリー記事

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com