この記事はフラー Advent Calendar 2019の 1 日目の記事です。
今年も JavaScript (TypeSctipt) をたくさん書きました。
JavaScript には ECMAScript という標準仕様がありますが,近年では TC39 によって毎年新しい機能が追加されています。議論のプロセスは常にオープンになっているので誰でも閲覧できます *。
そして今回は JavaScript の”Promise”にフォーカスして近況を解説します。
Promise で非同期プログラミング
Promise は ES2015 (ES6)で正式に導入されましたが,それ以前にも JavaScript プログラミングの非同期処理の中心にいました。
まずは Promise について簡単に復習しましょう。コンストラクタで Promise を生成する一般的な形式の例を以下に示します。
const promise = new Promise((resolve, reject) => {
doSomething(x => {
if (x) {
// 成功
resolve(x);
} else {
reject(new Error("失敗😢"));
}
});
});
promise
.then(result => { ... })
.catch(error => { ... });
doSomething
は何らかの非同期処理を行い,コールバックによって結果を得る関数です。無事に結果が得られたときは resolve()
を使います。もしも結果が得られなかったときは reject()
を使います。
Promise には”pending”, “fulfilled”, “rejected” の 3 つの状態が存在します。先ほどの例で,resolve()
か reject()
を呼びだす前の状態は pending 状態です。呼び出したあとは fulfilled か rejected のいずれかの状態になります(これを Settled と呼びます)。
Promise は必ず 3 つのうちいずれか状態をとりますが,Settled にならない場合もあります。例えば,以下の Promise は永久に pending 状態です。
const p = new Promise(() => {});
.finally()
ES2018 では Promise.prototype.finally()
が導入されています。
promise
.then(result => { ... })
.catch(error => { ... })
.finally(() => {
// 後始末をここに書く
});
.finally()
のコールバック関数は,fulfilled か rejected かに関わらず実行されます。
複数の Promise を操る Promise.all
と Promise.race
ES2015 では, 複数の Promise を操作する API Promise.all
と Promise.race
の 2 つが導入されました。
Promise.all
:全ての Promise が fulfilled となった結果Promise.race
:いずれかの Promise のうち最初に Settled となった結果
Promise.all
は複数の非同期処理を同時にスタートして,全てが完了したその結果を扱いたいときに使います。
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("成功😄"), 100);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("成功😁"), 200);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("成功😎"), 300);
});
Promise.all([p1, p2, p3])
.then(result => console.log(result)) // ["成功😄", "成功😁", "成功😎"]
.catch(e => console.log(e.message));
Promise.allSettled
と Promise.any
Promise.all
は,Promise が 1 つでも失敗して rejected
になったら,その結果は rejected
になってしまいます。
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("成功😄"), 100);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("成功😁"), 200);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("失敗😭")), 300);
});
Promise.all([p1, p2, p3])
.then(result => console.log(result))
.catch(e => console.log(e.message)); // "失敗😭"
そこで新たに 2 つの API が提案され,導入されようとしています。
Promise.allSettled
:全ての Promise が Settled となった結果Promise.any
:いずれかの Promise が最初に fulfilled となった結果。全ての Promise が rejected の場合にのみ,rejected 状態になる。
先ほどの例を Promise.allSettled
を使って書くと以下のようになります。
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("成功😄"), 100);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("成功😁"), 200);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("失敗😭")), 300);
});
Promise.allSettled([p1, p2, p3])
.then(result => console.log(result.map(r => r.value))) // ["成功😄", "成功😁", undefined]
.catch(e => console.log(e.message));
async
/ await
ES2017 で追加された Async 関数を使うと,Promise を更に扱いやすくなります。Async 関数は以下のように async
キーワードがついた関数です。
async function makePizza() {
const ingredients = await prepareIngredients(); // 材料を用意します
const pizza = await bakePizza(); // ピザを焼きます
return pizza;
}
Async 関数の中では,await
オペレータを使用できます。 await
は Promise が Settled になるまで待機し,結果が得られたら処理を再開します。もしもその Promise が fulfilled となった場合,await
はその値を返します。もしも rejected となった場合はそのエラーを throw します(try-catch 構文でハンドリングできます)。
Async 関数の結果は常に Promise です。したがって以下の 2 つの Async 関数は同じ値(”🍕“の Promise)を返します。
async function makePizza1() {
return "🍕";
}
async function makePizza2() {
return Promise.resolve("🍕");
}
const pizza1 = await makePizza1();
const pizza2 = await makePizza2();
console.log(pizza1 === pizza2); // true
await
は Promise ではない値も受け付けます。
const burger1 = await "🍔";
const burger2 = await Promise.resolve("🍔");
console.log(burger1 === burger2); // true
コールバック関数と await
まず最初に以下のヘルパー関数を定義します。
async function delayEcho(x, ms) {
return new Promise(resolve =>
setTimeout(() => {
resolve(x);
}, ms)
);
}
引数 x
で指定した値を引数ms
で指定した分だけ待って resolve する Async 関数です。
["🍔", "🍕", "🍣"]
という配列をこの関数と.map()
で処理したいとき,以下のように書きたくなるかもしれませんが,.map()
に渡している関数に async
キーワードが無いためシンタックスエラーです。
async function delayedFoods() {
const foods = ["🍔", "🍕", "🍣"];
return foods.map(f => await delayEcho(f, 100)); // シンタックスエラー 🙅♀️
}
const x = await delayedFoods();
console.log(x);
コードバック関数にも async
キーワードをつけるとシンタックスエラーは無くなりますが,先に述べたとおり Async 関数の返り値は Promise なので,正しく動作するには,Promise.all
を使って以下のように書きます。
async function delayedFoods() {
const foods = ["🍔", "🍕", "🍣"];
return Promise.all(foods.map(async f => await delayEcho(f, 100)));
}
const x = await delayedFoods();
console.log(x); // ["🍔", "🍕", "🍣"]
実はこの例は, delayEcho
の結果を await
する必要はなく以下のように書くとよりシンプルです。
async function delayedFoods() {
const foods = ["🍔", "🍕", "🍣"];
return Promise.all(foods.map(f => delayEcho(f, 100)));
}
Async イテレーション
最後に,ES2018 で追加された Async イテレーションをみていきます。
async function* collectFoods() {
await new Promise(resolve => setTimeout(() => resolve(), 1000));
yield delayEcho("🌭", 100);
yield delayEcho("🍜", 100);
yield "🍖";
}
collectFoods
は Async ジェネレータ関数です。await
に加えて yield
を使うことができます。collectFoods
は Async iterator を作成します。Async iterator は .next()
を呼び出すと結果が Promise で返ってきます。
const asyncIter = collectFoods();
console.log(await asyncIter.next()); // {value: "🌭", done: false}
console.log(await asyncIter.next()); // {value: "🍜", done: false}
console.log(await asyncIter.next()); // {value: "🍖", done: false}
console.log(await asyncIter.next()); // {value: undefined, done: true}
さらに,Async Iterator のループは for await...of
構文を使うことができます。
for await (const food of collectFoods()) {
console.log(food);
// "🌭"
// "🍜"
// "🍖"
}
ちなみに for await...of
構文は Async では無い通常の iterator でも正常に動作します。
for await (const food of ['🍎', '🍊', Promise.resolve('🍋')]) {
console.log(food);
// 🍎
// 🍊
// 🍋
}
おわりに
今年は,僭越ながら周囲の人に JavaScript を教える機会をいただくことが多い 1 年でした。特に Promise について質問されることが多かったので,この記事を書こうと思いつきました。来年も元気にやっていきますのでよろしくお願いします 👋