Old Sunset Days

Node.jsで非同期処理待ちを実現するPromise

日付 タグ node.js カテゴリ node.js

目次

Node.jsで非同期処理の完了待ちに使えるPromise

いままでCやC++の経験が長い自分にとってNode.jsで気をつけないといけなさそうなのがNode.jsの非同期処理。

例えば、あるファイルの読み込みを行って、その読み込み結果をコンソールログに出力したい場合を考える。

これを以下のようにすると期待通りに動かない。

read_test.js

const fs = require('fs');

fs.readFile("hoge.txt", 'utf8', (err, data) => {
  if(err){
    console.error(err);
    return;
  }
  console.log(data);
});

console.log('ファイル読み込み終わったよ');

先にファイルを読み込んで、
console.logでファイル読み込み終わったよ、
としたつもりで実行してみるとこうなってしまう。

$ node read_test.js
ファイル読み込み終わったよ
ほげほげテストだよ

ファイル読み込みが実際に完了する前に「ファイル読み込み終わったよ」メッセージがコンソールに出力されてしまう。

つまりプログラム中に出てくるのとは順番が逆順である。これはreadFile関数が非同期で処理されるため、I/O読み込みのようにちょっと時間のかかる処理は、そこで処理終了を待たず、次の命令に進んでしまうことによる。

readFileの場合は、同期関数版であるreadFileSyncを使えば、

read_sync_test.js

const fs = require('fs');
let data = fs.readFileSync('hoge.txt', 'utf8');
console.log(data);
console.log('ファイル読み込み終わったよ');

今度は順番通りのコンソール出力になる。

$ node read_sync_test.js
ほげほげテストだよ

ファイル読み込み終わったよ

他の例としては、
3秒待って最初の結果を表示してから、
2秒待って次の結果を表示して、
1秒待って最後の結果を表示したい場合に、

timer_test.js

unction myFunc(value){
  setTimeout(function(){
  console.log(`myFuncの実行中:引数${value}`);
  console.log(`${value}ms待ち完了`);
  console.log('=======');
  }, value);
}

myFunc(3000);
myFunc(2000);
myFunc(1000);
console.log('********')
console.log('全部が終了');
console.log(`********`)

こう書いたとする。しかし、実行結果は、

$ node timer_test.js
********
全部が終了
********
myFuncの実行中:引数1000
1000ms待ち完了
=======
myFuncの実行中:引数2000
2000ms待ち完了
=======
myFuncの実行中:引数3000
3000ms待ち完了
=======

非同期で実行されて次の命令を読み込んでしまうため、一番待ち時間が短いものから出力されてしまうので想定した順番通りにはならない。

このようにI/O待ちや通信結果待ちなど時間のかかりがちな処理が非同期で処理されているが、その結果が出るのを待って次の処理を行いたいケースは結構ある。

自分はNode.jsについて初学者レベルなので、その非同期処理をどう待つのが良いのか、調べていたのだが、Node.jsでは一般にPromiseというオブジェクトを利用することで解決するようだ。

Promiseとは何か?

PromiseはJavaScriptのES2015(ES6)から導入された仕様で、非同期処理の最終的な完了(もしくは失敗)状態と、結果の値を表すオブジェクト。

Promiseのインスタンスの作成では、resolveとrejectを引数として設定し、成功した場合はresolveを、失敗したらrejectを呼び出す。なお、rejectが不要ならば設定しなくてもいいようだ。

new Promise((resolve, reject)=>{
	// 成功ケース
    if(成功){
        resolve(成功時のvalue);
    }
    // 失敗ケース
    else {
        reject(失敗時のerror);
    };
});

このようにインスタンス化する。

Promiseのインスタンスは以下の3つのうちのどれかの状態をもつ。

  • Pending
  • Fulfilled
  • Rejected

初期状態で何も起きていない状態がPendingである。
成功時にresolveを呼ぶと状態=Fulfilledとなり、thenメソッドに設定された処理関数が呼び出される。
失敗時にrejectを呼ぶと状態=Rejectedとなり、catchメソッドに設定された処理関数が呼び出される。

Promiseを使った簡単な例

例として、与えられた引数が30未満かどうかで成功か失敗かが決まるPromiseオブジェクトがある場合

promise_sample1.js

function myFunc(value){
    return new Promise((resolve, reject)=>{
        if(value < 30){
            resolve('値は30未満です');
        } else {
            reject('値は30以上です');
        };
    });
};

myFunc(28).then((value, failue) => {
  console.log('実行成功');
  console.log('返り値:' + value);
}).catch((error) => {
  console.log('エラー発生');
  console.error(error);
});

これを実行すると28を引数として与えているので成功(resolve)->thenの流れで

$ node promise_sample1.js
実行成功
返り値:値は30未満です

これを引数を変えて32とするサンプルなら、失敗(reject)->catchの流れで

promise_sample2.js

function myFunc(value){
    return new Promise((resolve, reject)=>{
        if(value < 30){
            resolve('値は30未満です');
        } else {
            reject('値は30以上です');
        };
    });
};

myFunc(32).then((value, failue) => {
  console.log('実行成功');
  console.log('返り値:' + value);
}).catch((error) => {
  console.log('エラー発生');
  console.error(error);
});
$ node promise_sample2.js
エラー発生
値は30以上です

となり、Promiseの基本的な流れである
成功(resolve) => then
失敗(reject) => catch
という処理流れが確認できた。

このresolve -> thenという流れを利用することによって非同期処理の順番付けが可能になるのだ。

timer_promise_test.js

function myFuncPromise(value){
  return new Promise(function(resolve,reject){
    setTimeout(function(){
    console.log(`myFuncの実行中:引数${value}`);
    console.log(`${value}ms待ち完了`);
    console.log('=======');
    resolve('処理待ちしたよ');
    }, value);
  });
}

myFuncPromise(3000)
.then((value) => {
  console.log('全部が終了');
}).catch((error) => {
  console.log('エラー発生');
  console.error(error);
});

これにより、timer_test.jsでは想定通りにならなかった、
「全部が終了」よりも、
3秒待っての処理を先に完了させることができる。

$ node timer_promise_test.js
myFuncの実行中:引数3000
3000ms待ち完了
=======
全部が終了

また、thenは多段に繋げてチェーン化することができるので、処理を多段に繋げて処理順番を固定することが可能である。

Promiseを多段に繋げたチェーン処理

このthenを多段に繋げることで、最初の方で失敗した 3秒待ち処理 =>2秒待ち処理 =>1秒待ち処理 をこの順番で実現する方法を試してみる。

timer_promise_chain_test.js

function myFuncPromise(value){
  return new Promise(function(resolve,reject){
    setTimeout(function(){
    console.log(`myFuncの実行中:引数${value}`);
    console.log(`${value}ms待ち完了`);
    console.log('=======');
    resolve('処理待ちしたよ');
    }, value);
  });
}

myFuncPromise(3000)
.then((value) => {
  console.log(value);
  console.log('=>thenに処理が来ました=>');
  return myFuncPromise(2000);
})
.then((value) => {
  console.log(value);
  console.log('=>thenに処理が来ました=>');
  return myFuncPromise(1000);
})
.then((value) => {
  console.log('全部が終了');
}).catch((error) => {
  console.log('エラー発生');
  console.error(error);
});

実行結果は以下。then処理の中で次のPromiseをreturn返しすることで、thenチェーンを繋げていき、多段で順番処理を行うことができた。

$ node timer_promise_chain_test.js
myFuncの実行中:引数3000
3000ms待ち完了
=======
処理待ちしたよ
=>thenに処理が来ました=>
myFuncの実行中:引数2000
2000ms待ち完了
=======
処理待ちしたよ
=>thenに処理が来ました=>
myFuncの実行中:引数1000
1000ms待ち完了
=======
全部が終了

以上、Node.jsの初学者である自分が非同期処理をどうやって待つのかPromiseを使って試した次第だが、 このPromiseとasync/awaitを組み合わせると処理ロジックがより明快に記述できるらしいので、そちらについても今後のためにテストや勉強をしておきたいと思っている。