情報アイランド

「情報を制する者は世界を制す」をモットーに様々な情報を提供することを目指すブログです。現在はプログラミング関連情報が多めですが、投資関連情報も取り扱っていきたいです。

Node.jsでプロミスを使用する

2016/09/02

プロミスとは

プロミスとは非同期的な処理を表すオブジェクトです。

非同期的な処理を表す古典的な方法には非同期コールバック関数がありました。あるいは、非同期イベントがありました。

しかし、非同期コールバック関数には幾つかの問題点がありました。

  • 非同期コールバック関数を多用すると無名関数の何段ものネストが発生しやすいです。これをコールバック地獄と言います。ネストが深くなり過ぎると可読性が悪くなります。
  • エラー処理の方法が増えることにより、処理を記述する際に例外が発生した場合に加えて非同期コールバック関数にエラーオブジェクトが渡された場合も考慮しなければならなくなります。
  • 関数の入力と出力が分かり辛くなります。非同期コールバック関数は関数の出力に係るものであるにも拘らず関数の入力を表す引数の1つとして受け取ることになります。
  • 複数の関数を合成するのが難しくなります。

プロミスを使用するとこのような問題点を解決することができます

非同期コールバック関数を引数として受け取る関数による非同期的な処理の表現は下のようなものでした。

function asyncfunc (arg1, arg2, callback) {
    callback(null, 'xxx');
    //or
    callback(new Error('yyy'), null);
}

プロミスによる非同期的な処理の表現は下のようなものになります。

var p = new Promise(function (resolve, reject) {
    resolve('xxx');
    //or
    resolve(new Promise(function (resolve, reject) {
        //...
    }));
    //or
    reject(new Error('yyy'));
    //or
    throw new Error('zzz');
});

プロミスの状態

プロミスは作成された時点では保留状態、すなわち、プロミスが表す非同期的な処理が完了も中断もしていない状態にあります。

非同期的な処理が実行され、完了すると、完了状態になります。この場合、プロミスは結果と呼ばれる値を1つ有します。これは非同期的な処理の結果を表します。

あるいは、非同期的な処理が実行されたものの、実行中にエラーが発生すると、エラー状態になります。この場合、プロミスはエラーオブジェクトを1つ有します。これは発生したエラーの内容を表します。

一度プロミスが完了状態やエラー状態になるともう別の状態に変化することはありません。

プロミスの使用

プロミスを使用するには下のようなモジュールのプロミスクラスを利用する方法があります(プロミスクラスを提供するモジュールには他にも取り上げ切れない程に多くのものがあります。ここでは主なもののみを挙げています)。

  • promiseモジュール・・・promiseクラス
  • rsvpモジュール・・・rsvp.Promiseクラス
  • bluebirdモジュール・・・bluebirdクラス
  • qモジュール・・・q.Promiseクラス
  • whenモジュール・・・when.promiseクラス

また、ECMAScript 6で追加されたPromiseクラスを使用することもできます。

以後プロミスをpで表すものとします。

プロミスの作成

プロミスを作成するにはプロミスクラスのコンストラクタを使用します。ただし、q.Promiseクラスとwhen.promiseクラスの場合、new演算子は使用しません。

var promise = require('promise');

var p = new promise(function (resolve, reject) {
    resolve('xxx');
    //or
    resolve(new promise(function (resolve, reject) {
        //...
    }));
    //or
    reject(new Error('yyy'));
    //or
    throw new Error('zzz');
});
var q = require('q');

var p = q.Promise(function (resolve, reject) {
    resolve('xxx');
    //or
    resolve(q.Promise(function (resolve, reject) {
        //...
    }));
    //or
    reject(new Error('yyy'));
    //or
    throw new Error('zzz');
});

第1引数に非同期的な処理を関数として指定します。

この関数の第1引数は処理が完了した場合に呼び出す関数です。この引数の名称はresolveとされることが多いです。以後この引数をresolveと表記します。

resolve関数を呼び出す際には第1引数に結果を指定します。この場合、このプロミスは保留状態から完了状態に変わります。

あるいは、結果を指定する代わりに別のプロミスを指定することもできます。この場合、別のプロミスに処理を移譲することができます。

第2引数は処理の実行中にエラーが発生した場合に呼び出す関数です。この引数の名称はrejectとされることが多いです。以後この引数をrejectと表記します。

この関数を呼び出す際には第1引数にエラーオブジェクトを指定します。この場合、このプロミスは保留状態からエラー状態に変わります。

また、処理の実行中に例外が発生した場合にもプロミスは保留状態からエラー状態に変わります。

なお、これらのコンストラクタにより作成されたプロミスは作成されてすぐに非同期的な処理を開始します

状態変化時の処理

プロミスが保留状態から完了状態やエラー状態に変わった時に処理を行うにはp.done関数かp.finally関数を使用します。

p.done関数は下のプロミスクラスで提供されています。

  • promiseクラス
  • bluebirdクラス
  • q.Promiseクラス
  • when.promiseクラス
p.done(function (result) {
}, function (err) {
});

第1引数にプロミスが保留状態から完了状態に変わった時に非同期的に呼び出される関数を指定します。この関数の第1引数は結果です。

第2引数にプロミスが保留状態からエラー状態に変わった時に非同期的に呼び出される関数を指定します。この関数の第1引数はエラーオブジェクトです。

p.finally関数は下のプロミスクラスで提供されています。

  • rsvp.Promiseクラス
  • bluebirdクラス
  • q.Promiseクラス
  • when.promiseクラス
p.finally(function () {
});

第1引数にプロミスが保留状態から完了状態やエラー状態に変わった時に非同期的に呼び出される関数を指定します。

p.done関数やp.finally関数が提供されていないプロミスクラスにおいては代わりにp.then関数やp.catch関数を使用しますp.then関数やp.catch関数に関しては別の記事で解説します)。

完了状態のプロミスの作成

完了状態のプロミスを作成するにはプロミスクラスのresolve関数を使用します。

resolve関数は全てのプロミスクラスで提供されています(ただし、whenモジュールの場合、when.resolve関数となります)。

var p = promise.resolve('xxx');

第1引数に結果を指定します。

あるいは、結果を指定する代わりに別のプロミスを指定することもできます。この場合、別のプロミスに処理を移譲することができます。

返り値としてプロミスが得られます。

エラー状態のプロミスの作成

エラー状態のプロミスを作成するにはプロミスクラスのreject関数を使用します。

reject関数は全てのプロミスクラスで提供されています(ただし、whenモジュールの場合、when.reject関数となります)。

var p = promise.reject(new Error('xxx'));

第1引数にエラーオブジェクトを指定します。

返り値としてプロミスが得られます。

サンプルコード1

p.done関数の第1引数や第2引数の関数が非同期的に呼び出されることを確認するコードです。

promise-done-1.js

var promise = require('promise');
var util = require('util');

var pres = new promise(function (resolve, reject) {
    util.log('task 1 complete.');
    resolve('task 1');
});

var prej = new promise(function (resolve, reject) {
    util.log('task 2 complete.');
    reject(new Error('task 2'));
});

pres.done(function (result) {
    util.log(result);
}, function (err) {
    console.error(err);
});

prej.done(function (result) {
    util.log(result);
}, function (err) {
    console.error(err);
});

util.log('program started.');

使用パッケージ

  • promise
    npm install promiseでインストールします。

実行結果

program started.というメッセージが表示される前にtask 1 complete.というメッセージやtask 2 complete.というメッセージが表示されていることが分かります。

また、program started.というメッセージが表示された後にtask 1というメッセージやエラーメッセージが表示されていることが分かります。

すなわち、p.done関数の第1引数や第2引数の関数が非同期的に呼び出されていることが分かります。

C:\work\node>node promise-done-1.js
23 May 13:02:36 - task 1 complete.
23 May 13:02:36 - task 2 complete.
23 May 13:02:36 - program started.
23 May 13:02:36 - task 1
Error: task 2
    at C:\work\node\promise-done-1.js:11:9
    at tryCallTwo (C:\work\node\node_modules\promise\lib\core.js:45:5)
    at doResolve (C:\work\node\node_modules\promise\lib\core.js:200:13)
    at new Promise (C:\work\node\node_modules\promise\lib\core.js:66:3)
    at Object.<anonymous> (C:\work\node\promise-done-1.js:9:12)
    at Module._compile (module.js:541:32)
    at Object.Module._extensions..js (module.js:550:10)
    at Module.load (module.js:456:32)
    at tryModuleLoad (module.js:415:12)
    at Function.Module._load (module.js:407:3)

サンプルコード2

promiseクラスのコンストラクタの第1引数に指定した関数が非同期的な処理を含んでいる例です。

promise-done-2.js

var promise = require('promise');
var util = require('util');

var pres = new promise(function (resolve, reject) {
    util.log('task 1 start.');
    setTimeout(function () {
        util.log('task 1 complete.');
        resolve('task 1');
    }, 10000);
});

var prej = new promise(function (resolve, reject) {
    util.log('task 2 start.');
    setTimeout(function () {
        util.log('task 2 complete.');
        reject(new Error('task 2'));
    }, 5000);
});

pres.done(function (result) {
    util.log(result);
}, function (err) {
    console.error(err);
});

prej.done(function (result) {
    util.log(result);
}, function (err) {
    console.error(err);
});

使用パッケージ

  • promise
    npm install promiseでインストールします。

実行結果

C:\work\node>node promise-done-2.js
23 May 12:51:58 - task 1 start.
23 May 12:51:58 - task 2 start.
23 May 12:52:03 - task 2 complete.
Error: task 2
    at Timeout._onTimeout (C:\work\node\promise-done.js:16:10)
    at tryOnTimeout (timers.js:224:11)
    at Timer.listOnTimeout (timers.js:198:5)
23 May 12:52:08 - task 1 complete.
23 May 12:52:08 - task 1

サンプルコード3

promise.resolve関数とpromise.reject関数の使用例です。

promise-resolve-reject.js

var promise = require('promise');
var util = require('util');

var pres = promise.resolve('task 1');
var prej = promise.reject(new Error('task 2'));

pres.done(function (result) {
    util.log(result);
}, function (err) {
    console.error(err);
});

prej.done(function (result) {
    util.log(result);
}, function (err) {
    console.error(err);
});

util.log('program started.');

使用パッケージ

  • promise
    npm install promiseでインストールします。

実行結果

C:\work\node>node promise-resolve-reject.js
23 May 15:06:45 - program started.
23 May 15:06:45 - task 1
Error: task 2
    at Object.<anonymous> (C:\work\node\promise-resolve-reject.js:5:27)
    at Module._compile (module.js:541:32)
    at Object.Module._extensions..js (module.js:550:10)
    at Module.load (module.js:456:32)
    at tryModuleLoad (module.js:415:12)
    at Function.Module._load (module.js:407:3)
    at Function.Module.runMain (module.js:575:10)
    at startup (node.js:159:18)
    at node.js:444:3

関連

pizyumi
プログラミング歴19年のベテランプログラマー。業務システム全般何でも作れます。現在はWeb系の技術を勉強中。
スポンサーリンク

-Node.js