JavaScript
bluebird

Bluebird で Promise を学ぶ

More than 1 year has passed since last update.

概要

Bluebird は Promise を扱うためのライブラリです。チュートリアルや stackoverflow の回答が充実しており、初心者が学びやすいライブラリです。TC39 で提案されている機能があるので、将来の先取り学習ができます。

Bluebird は Observable を学ぶための準備教材としても評価できます。Observable は Promise、EventEmitter、ストリームなどの複数の概念を統合する概念です。Observable を扱うためのライブラリとして RxJS や xstream が挙げられます。Observable は TC39 で提案されており、将来の標準機能になる可能性があります。

Bluebird をブラウザー版アプリで採用するときの課題はライブラリのファイルの大きさです。より小さなサイズのライブラリに切り換えるか、個別の機能に特化したライブラリを導入するか検討する必要があるでしょう。

到達目標

学習の到達目標として次の項目を挙げます。

  • Bluebird の機能を一通り使える
  • ES2017 の async/await を使ったコードに書き換えられる
  • 主要なテストツールでコードが書ける
  • Observable ライブラリ (RxJS) を使ったコードに書き換える

インストール

Yarn でインストールする方法は次のとおりです。

yarn add bluebird

標準の Promise のオーバーライド

最新の Node.js やブラウザーでは Promise が標準搭載されています。オーバーライドするには次のようにコードを書きます。

const Promise = require("bluebird");

Promise 対応の HTTP クライアントのなかには Bluebird などの標準ではない Promise に切り換えることができるものがあります。

生成

resolve と reject

resolve は fulfilled の状態の Promise を返し、reject は rejected の状態の Promise を返します。どちらもコンストラクターを簡略化させたファクトリメソッドとして考えることができます。

const p = Promise.resolve("Hello");
p.then((value) => console.log(value));

const p2 = Promise.reject(new Error("Eror"));
p2.catch((e) => console.log(e));

同じ内容をコンストラクターメソッドで次のように書くことができます。

const p = new Promise((resolve) => {
  resolve("Hello");
});

p.then((value) => { console.log(value); });

const p2 = new Promise((resolve, reject) => {
  reject(new Error("Error"));
});

p2.catch((value) => { console.log(value); });

try

try メソッドはコンストラクターメソッドの簡略バージョンです。

const p = Promise.try(() => { return "Hello"; });
p.then((value) => console.log(value));

const p2 = Promise.try(() => { throw new Error("Error"); });
p2.catch((value) => console.log(value));

tap と tapCatch

taptapCatch はメソッドチェーンの途中の値を調べるために使います。長いメソッドチェーンの途中でトラブルを探すときに便利です。

const p = Promise.resolve(0);
p.then(x => x + 1)
 .then(x => x + 2)
 .tap(console.log)
 .then(x => x + 3)
const p = Promise.resolve(0);

p.then(x => x + 1)
 .then(x => { throw new Error("Error"); })
 .tapCatch(console.log)
 .then(x => x + 3)
 .catch((e) => { console.log("catch: "); });

catch、error、finally

const p = Promise.resolve("Hello");

p.then((value) => { console.log("then", value); })
 .catch((e) => { console.log("catch", e); })
 .finally(() => { console.log("finally"); });

async/await で try/catch/finally 文に書き換えてみましょう。

(async () => {
  try {
    const msg = await Promise.resolve("Hello");
    console.log(msg);
  } catch (e) {
    console.log("catch", e);
  } finally {
    console.log("finally");
  }
})();

1つのエラーごとに1つの catch を使うことができます。catch の第1引数で補足したいエラーオブジェクトを指定します。

const { Promise, TimeoutError, OperationalError } = require("bluebird");

const p = Promise.reject(new OperationalError("Hello"));

p.then((value) => { console.log("then", value); })
 .catch(TimeoutError, (e) => { console.log("タイムアウトエラー", e); })
 .catch(OperationalError, (e) => { console.log("オペレーションエラー", e); })
 .finally(() => { console.log("finally"); });

error は OperationalError に特化したショートカットメソッドです。

p.then((value) => { console.log("then", value); })
 .catch(TimeoutError, (e) => { console.log("タイムアウトエラー", e); })
 .error((e) => { console.log("オペレーションエラー", e); })
 .finally(() => { console.log("finally"); });

delay

delay は引数で指定した時間だけ処理を遅延させます。

const p = Promise.resolve("Hello");

p.tap(console.log)
 .delay(3000)
 .tap(console.log);

async/await 構文を使えばサーバーサイドの sleep 関数と同じ結果を実現できます。

(async () => {
  console.log("Hello");
  await Promise.resolve().delay(3000);
  console.log("Hello");
})();

手軽に処理の時間差をつくることができるので、非同期処理を学ぶための重要な教材です。

timeout と cancel

timeout で指定した時間を超えるとエラーになります。

const p = Promise.resolve("Hello").delay(3000);

p.timeout(1000)
 .then((value) => { console.log("then", value); })
 .catch((e) => { console.log("catch", e); });

キャンセルを実行すると

Promise のコンストラクターに渡すコールバックの第3引数 (onCancel) のメソッドにキャンセルで実行される内容を渡します。

const Promise = require("bluebird");

Promise.config({
  cancellation: true
});

const promise = new Promise((resolve, reject, onCancel) => {
  const id = setTimeout(resolve, 1000);
  onCancel(() => clearTimeout(id));
});

// 実行されません。
promise.then(() => console.log("then"));

promise.cancel();

キャンセルを実行できるようにするには Promise.config で Bluebird の設定を変更する必要があります。

all と join

all は複数の Promise の結果をまとめるために使います。

const one = Promise.resolve(1);
const two = Promise.resolve(2);
const three = Promise.resolve(3);

Promise.all([one, two, three]).then((values) => {
  console.log(values.reduce((v, i) => v + i));
});

then で受け取る引数を配列展開することもできます。

Promise.all([one, two, three]).then(([one, two, three]) => {
  console.log(one + two + three);
});

async/await で書き換えると次のようになります。

(async () => {
  const one = await Promise.resolve(1);
  const two = await Promise.resolve(2);
  const three = await Promise.resolve(3);

  console.log(one + two + three);
})();

join の引数は配列ではなく可変です。TypeScript でパフォーマンスを改善したい場合に選ぶとよいでしょう。

const join = Promise.join;

const one = Promise.resolve(1);
const two = Promise.resolve(2);
const three = Promise.resolve(3);

join(one, two, three).then((values) => {
  console.log(values.reduce((v, i) => v + i));
});

テスト

Jest

async/await の構文はかんたんに使えます。

test.js
it('works with async/await', async () => {
  const data = await Promise.resolve('Hello');
  expect(data).toEqual('Hello');
});

Jest を Yarn でプロジェクトに追加するには次のコマンドを実行します。

yarn add --dev jest

npm 5.2.0 で導入された npx を使って Jest を実行してみましょう。

npx jest test.js

その他

エラーに文字列を避ける

done は非推奨

done は then と似ていますが、戻り値は Promise ではなく undefined なので、その後のメソッドチェーンをつなげることができません。言い換えると再利用を妨げます。

不変オブジェクト

Promise の状態が fulfilled もしくは rejected に確定した状況であれば、Promise を不変オブジェクトとして扱うことができます。不変オブジェクトは変数で宣言した箇所で値が決まるので、想定しない結果の値が得られたときに調べやすくなります。

const p = Promise.resolve(0);
const p2 = p2.then(x => x + 1);
const p3 = p.then(x => x + 2);

// 0
p.then(value => console.log(value));

// 1
p2.then(value => console.log(value));

// 3
p3.then(value => console.log(value));

なお ES2015 で導入された const で変数を宣言しても不変オブジェクトにはなりません。次のようにプロパティを変更できます。

const foo = {};
foo.bar = "baz";
console.log(foo.bar);
// baz

プロパティの変更を禁止するには Object.freeze を使います。

"use strict";

const Promise = require("bluebird");


const foo = Object.freeze({bar: "baz"});
foo.bar = "qux";

入れ子のオブジェクトに対応したいのであれば、Object.freeze を再帰的に適用する関数を定義する必要があります。パッケージとして substack/deep-freeze が配布されています。

複数の関数に分割する

Promise のメソッドの戻り値は Promise なので、メソッドチェーンを書くことができます。

let request = Promise.resolve(0);
let response = request
  .then(x => x + 1)
  .then(x => x + 2);

response.then(value => console.log(value));

メソッドチェーンが長くなったり、複数の関心事が含まれるようになった場合、引数と戻り値の両方が Promise である関数に分離することができます。次のコードは MVC レイヤーを単純化したものです。

const p = Promise.resolve(0);
const p2 = p.then(x => x + 1);
const p3 = p2.then(x => x + 2);

let request = Promise.resolve(0);
let response = controller(request);
response = model(response);
view(response);

function controller(p) {
  return p.then(x => x + 1);
}

function model(p) {
  return p.then(x => x + 2);
}

function view(p) {
  p.then(value => console.log(value));
}

引数と戻り値が同じ種類のオブジェクトというアイデアは Web アプリケーションの HTTP ミドルウェアで見ることができます。

読み物

Observable の練習

以前 Qiita に投稿した Observable の記事です。Observable は Promise、Event Emitter、ストリームなどを統合する概念なので、これまで日常でやってきたことをObservable に置き換えることから始めるとよいでしょう。