ほんじゃら堂

めんどくさい仕事をラクにする作業自動化レシピ集

node.jsがやたら非同期化しようとするのをasync/awaitでどうにか同期化する

node.jsのライブラリって

非同期的に実行できるようにしてくれてるものがとても多い。

きっとそれは大変ありがたいことなのだけど、

とりあえず動いてくれればそれで良いような

意識の低いコマンドラインツールを作るような時は

思ったような順番で処理が実行されなかったり、

コールバック地獄になったりで

「ただ同期的に動かしたいだけなのに!」と困ることがよくある。

何ならnode.jsを使う時の悩みの大半は

この非同期処理の部分なんじゃないだろうかと思うぐらい。

このままではnode.jsが好きになれん、

ということであれこれ試してみた結果、

v7.6から使えるようになったasync/awaitを

使う形でようやく自分なりのパターンが見えてきたので、

ここでまとめておきたい。

async/awaitの説明

ここからの説明は

「多分こうなんじゃない?」レベルのものなので、

正確な情報についてはまた別途調べてみていただきたい。

さて、async/awaitはどういうものかというと、

「非同期な関数」に「await」をつけると非同期な関数の処理が終わるまで

待ってから次の処理を行ってくれる、というものだ。

例えば「asyncFunc()」という非同期な関数がある場合に、

let result = await asyncFunc();

という形で書くことで、

「asyncFunc()」の実行が終わるのを待ってから

resultに結果を入れて次の処理に進んでくれるようになる。

awaitをつけるだけで、

asyncFuncにコールバック関数を仕込んだりする必要がなく、

普通の非同期でない関数と同じように結果を受け取ることができる。

「await」は「async」な関数の中でのみ使用できる、というルールがある。

なので、実際に使う時は下記のような形になる。

async function main() {
    let result = await asyncFunc();
}

main();

awaitをつけるだけでどんな非同期関数でも同期化できたら簡単なのだけど、

awaitを使って同期化できる関数には条件がある。

それは、「Promise」オブジェクトを返すということ。

Promiseオブジェクトは

「そのうち実行することを約束された処理を持ったオブジェクト」

という感じのもので、Promiseオブジェクトを返す非同期関数は下記のようなもの。

function asyncFunc2() {
    return new Promise((resolve) => {
        // ...何かしらの時間がかかる処理...
        let result = '返したい値';

        resolve(result);
    });
}

非同期関数が返すPromiseオブジェクトのthenメソッドに

Promiseオブジェクト内の処理後に実行したい処理を指定しておくと、

実行してくれる。

asyncFunc2().then((result) => {
    // Promiseのresolve関数実行後に実行したい処理
    console.log(result);
});

これがasync/awaitを使用せずにPromiseを使うパターン。

コールバック関数を重ねる形ではないが、やはりコールバック関数が必要となる。

一方、async/awaitを使うと、thenメソッドを定義する代わりに

関数の戻り値として直接resolve関数の結果を受け取ることができる。

function asyncFunc2() {
    return new Promise((resolve) => {
        // ...何かしらの時間がかかる処理...
        let result = '返したい値';

        resolve(result);
    });
}

async function main() {
    const result = await asyncFunc2();
    console.log(result);
}

main();

処理内容は同じだけど、async/awaitを使った方が

コールバック感がなくなり、コードが同期的で分かりやすい感じになる。

例えば一定時間処理を停止するsleep関数を作って使いたい時とかは

下記のような関数を作成しておき、

function sleep(waitMillSeconds: number) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, waitMillSeconds);
    });
}

awaitで呼び出せばシンプルにsleep処理を実行できる。

async function main() {
    console.log('開始');

    // 1秒待機
    await sleep(1000);

    console.log('終了');
}

main();

最近のライブラリはだいたいPromise形式の返り値に対応しているので、

そういったライブラリはthenを使う代わりにasync/awaitを使えばすっきり書ける。

理解しきれてない「async」定義

ところで上に書いたとおり、awaitはasyncな関数内でしか使えないという制限がある。

「async」な関数とは何かというと、「返り値が必ずPromiseになる関数」らしい。

例えばPromiseオブジェクトを明示的に返さない関数でも

function func3() {
    return 'hello';
}

async function func4() {
    return 'hello';
}

asyncをつけるとPromiseを返すようになる。

// hello
const res3 = func3();

// Promise
const res4 = func4();

// hello
const res4_2 = await func4();

なので、「await」を1回でも使おうと思ったらそれをラップする関数は

全部「async」な関数にする必要がある。

async function asyncFunc5() {
    return await asyncFunc();
}

async function asyncFunc6() {
    return await asyncFunc5();
}

async main() {
    const result = await asyncFunc6();
}

main();

もはや「async」ってわざわざ書く必要ある?みんなこう書いてるの?

ってなるけど、調べきれてないので、

おまじないとして一旦受け入れた。

Promise非対応の非同期関数への対応

async/awaitはPromiseオブジェクトを返す非同期関数を前提としたものなので、

Promiseを返さない非同期関数には使えない。

使いたい時は、自分でラッパー関数を作ることになる。

例えばファイルを行ごとにリストにして読み込みたい時なんかは、

下記のような関数を定義するとasync/awaitで呼べるようになる。

import fs from 'fs';
import readline from 'readline';

function readFileLines(srcFilePath, encoding) {
    let rs = fs.createReadStream(srcFilePath, {encoding: encoding});
    let rl = readline.createInterface({input: rs});

    return new Promise((resolve, reject) => {
        const lineList = [];
        rl.on('line', (line) => {
            lineList.push(line);
        })
        .on('close', () => {
            resolve(lineList)
        });
    });
}

おわり

まだ理解しきれていない部分はあるものの、

Promiseとasync/awaitの概念をこれぐらい押さえておけば

同期的に処理を行うツールをつくる時に困ることがだいぶ減るはず。

実践Node.jsプログラミング Programmer's SELECTION

実践Node.jsプログラミング Programmer's SELECTION