概要
コピペで結果を確認しながら学べる Promise の学習教材を書きました。結果のふるまいを予測しやすいようにコードを短くし、ネットワークに依存しないようにしました。ES2015 のアロー関数や ES2017 の async/await にも対応した書き方を示しています。
値を決め打ちする
resolve と reject
問題をかんたんにするために成功もしくは失敗の値を決め打ちにしましょう。最初は Promise.resolve
で成功の場合を試してみましょう。
const p = Promise.resolve("Hello Promise");
p.then(value => {
console.log("then");
console.log(value);
})
.catch(value => {
console.log("catch");
console.log(value);
});
結果は次のようになります。
then
Hello Promise
次は Promise.reject
で失敗の場合を扱ってみましょう。
const p = Promise.reject("Hello Promise");
p.then(value => {
console.log("then");
console.log(value);
})
.catch(value => {
console.log("catch");
console.log(value);
});
async/await 対応に書き換える
今度は async/await 対応に書き換えてみましょう。まずは Promise.resolve
の場合です。
async function main() {
try {
const text = await Promise.resolve("Hello Promise");
console.log("try");
console.log(text);
} catch(err) {
console.log("catch");
console.log(err);
}
}
main();
結果は次のようになります。
try
Hello Promise
今度は Promise.reject
に取り組んでみましょう。
async function main() {
try {
const text = await Promise.reject("Hello Promise");
console.log("try");
console.log(text);
} catch(err) {
console.log("catch");
console.log(err);
}
}
main();
結果は次のようになります。
catch
Hello Promise
関数の宣言による名前空間の汚染を減らすために関数の即時実行を使うこともできます。
(async () => {
try {
const text = await Promise.resolve("Hello Promise");
console.log("try");
console.log(text);
} catch(err) {
console.log("catch");
console.log(err);
}
})();
コンストラクター関数呼び出しへの書き換え
Promise.resolve
をコンストラクター関数呼び出しを使ったコードに書き換えてみましょう。
const p = Promise.resolve("Hello Promise");
const p2 = new Promise((resolve, reject) => {
return resolve("Hello Promise");
});
次に Promise.reject
を使うコードを書き換えてみましょう。
const p = Promise.reject("Hello Promise");
const p2 = new Promise((resolve, reject) => {
return reject("Hello Promise");
});
Promise.try を使う
Promise.try
は TC39 で提案されているメソッドです。Promise のコンストラクターで指定するコールバックの内容を簡略化することが目的です。次のコードで試すことができます。
// https://twitter.com/RReverser/status/695678489937186816
Promise.try = function(cb) { return new this(r => r(cb())) };
Polyfill のコードは TC39 のリポジトリをご参照ください。Bluebird にも実装が存在します。
Promise.resolve
は次のように書き換えることができます。
const p = Promise.resolve('Hello Promise');
const p2 = Promise.try(() => {
return "Hello Promise";
});
Promise.reject
は次のように書き換えることができます。
const p = Promise.reject(new Error('Error'));
const p2 = Promise.try(() => {
throw new Error("Error");
});
実行のたびに結果が変わるようにする
今度はコンストラクター関数を使ったやり方で実行のたびに結果が変わるようにしてみましょう。時刻の秒数が偶数か奇数を判定する関数を使ってみます。
function isEven() {
let seconds = (new Date().getSeconds());
return seconds % 2 === 0;
}
const p = new Promise((resolve, reject) => {
const text = "Hello Promise";
return isEven() ? resolve(text) : reject(text);
});
p.then(value => {
console.log("then");
console.log(value);
})
.catch(value => {
console.log("catch");
console.log(value);
});
Promise.prototype.finally
TC39 で Promise.prototype.finally
が提案されています。Node.js で試験的に実装されている (--harmony_promise_finally
) ほかに TC39 のリポジトリで Polyfill も一緒に公開されています。Bluebird でも実装されています。
const p = Promise.resolve("Hello")
.then(value => console.log("then", value))
.finally(() => console.log("finally"));
Node.js で実行するには次のようにフラグを指定します。
node --harmony_promise_finally test.js
async/await を使って書き換えると次のようになります。
async function run() {
try {
const msg = await Promise.resolve("Hello");
console.log("then", msg);
} finally {
console.log("finally")
}
}
run();
キャンセル
ユーザーの入力をもとに Promise の実行をキャンセルさせたいことがあります。ES2016 で対応された Promise にない機能なので、Bluebird のようなライブラリを導入する必要があります。
const Bluebird = require('bluebird');
const p = Bluebird.resolve('成功');
p
.then(value => {
console.log("then");
if (!p.isCancelled()) {
console.log("then でキャンセルを検出しました。");
}
})
.catch(() => {
console.log('catch');
if (!p.isCancelled()) {
console.log("catch でキャンセルを検出しました。");
}
})
.finally(() => {
console.log('finally');
if (!p.isCancelled()) {
console.log("finally でキャンセルを検出しました。");
}
});
p.cancel();
モジュールの動的なインポート
モジュールを動的にインポートできるようにする機能が TC39 (dynamic) で提案されています。2017年6月時点でステージ3の段階にあり、Webpack 2 および TypeScript 2.4 でサポートされます。Node.js には --harmony-dynamic-import
フラグがありますが、2017年6月時点ではエラーになりました。
今回は TypeScript で試してみましょう。import
文の戻り値は Promise です。
import('./hello').then(hello => {
console.log(hello.getGreeting());
});
export function getGreeting() {
return "Hello World";
}
記事を書いた時点では TypeScript 2.4 RC が公開されていたので、次のコマンドでインストールしました。
npm install -g typescript@rc
npm install @types/node
コンパイルと実行を同時に行うために ts-node を導入します。
npm install -g ts-node
tsc
でコンパイルする際には --lib
オプションを指定します。
tsc --lib ES2015 main.ts
ts-node
を使う場合、--compilerOptions
でコンパイルオプションを指定します。
ts-node --compilerOptions '{"target":"es2015"}' main.ts
async/await を使って書き直してみましょう。
async function main() {
const {getGreeting} = await import('./hello');
console.log(getGreeting());
}
main();
実行を遅延させる
setTimeout
を使って実行を遅延させる関数を定義してみましょう。then
で指定できるように Promise を返すことにします。後からいろいろなメソッドチェーンをつけ加えることができます。Bluebird や RxJS は delay をあらかじめ定義しています。
// https://gist.github.com/joepie91/2664c85a744e6bd0629c
function delay(time) {
return (result) => {
return new Promise(resolve => setTimeout(() => resolve(result), time))
};
}
Promise.resolve("Hello")
.then(delay(2000))
.then(result => console.log(result));
async/await で同期的に処理を停止させる (sleep)
async/await を使って一定時間のあいだ同期的に処理を停止させることができます。
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
(async () => {
console.log("スタート");
await sleep(1000);
console.log("1秒経過")
})();
async/await を使わないのであれば、new Date().getTime()
を使って時刻を確認するやり方があります。
// https://stackoverflow.com/a/17532524/531320
function sleep(ms) {
let start = new Date().getTime(), expire = start + ms;
while (new Date().getTime() < expire) { }
return;
}
console.log("開始");
sleep(1000);
console.log("1秒経過");