chooblarin’s blog

2019年のPromise

December 01, 2019

  • #JavaScript

この記事はフラー 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 状態です。呼び出したあとは fulfilledrejected のいずれかの状態になります(これを Settled と呼びます)。

promise has 3 states

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.allPromise.race

ES2015 では, 複数の Promise を操作する API Promise.allPromise.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.allSettledPromise.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 について質問されることが多かったので,この記事を書こうと思いつきました。来年も元気にやっていきますのでよろしくお願いします 👋

参考