概要
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
tap
と tapCatch
はメソッドチェーンの途中の値を調べるために使います。長いメソッドチェーンの途中でトラブルを探すときに便利です。
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 の構文はかんたんに使えます。
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 ミドルウェアで見ることができます。
読み物
- awesome-promises - 個別の機能に特化したライブラリを探すのに便利です。
- Promise/A+仕様を、チュートリアル形式で詳しく解説します 仕様を満たす実装は以外とあっさりしたものだということを学べます。
- Promiseはどう動作するのか Promise 入門の説明がわかりやすいです。
- 命令型のコールバック、関数型のプロミス: Node が逸した最大の機会 LazyPromise によるモジュールローダーの解説があります。
- JavaScriptのモナド 継続モナドとしての Promise の解説があります。
- Writing Promise-Using Specifications 英語のドキュメントですが、Promise が適切なケースなど具体的でわかりやすい解説です。W3C の仕様を追いかけていれば著者の Domenic さんの名前をたまに見ることがあるでしょう。
- Promise Cancellation Is Dead — Long Live Promise Cancellation! Promise ライブラリおよび RxJS でのキャンセルの実現方法の解説です。著者の Ben Lesh さんは RxJS の開発者です。
Observable の練習
以前 Qiita に投稿した Observable の記事です。Observable は Promise、Event Emitter、ストリームなどを統合する概念なので、これまで日常でやってきたことをObservable に置き換えることから始めるとよいでしょう。