UUUM攻殻機動隊(エンジニアブログ)

UUUMのエンジニアによる技術ブログです

Promise入門しました

はじめまして

はじめまして!先月よりUUUMに入社した新米エンジニアの赤根谷です。 会社に入るまでPromiseやasync, awaitについてほとんど触っていなかったのですが、会社で必要になったのでこの度勉強しました(既存のプロジェクトにたくさん使われていました)。

Promise、async, awaitについて学んだことを社内の勉強会で共有したので、今回はそのうちのPromise部分についてブログにまとめようと思います。

一応の目標は、Promise学習の初期段階でつまづかない土台を作ることです。

注:Promise, async, awaitについて全く知らないという以前の僕のような方のためにざっくり言っておくと、これらは新しいJavaScriptで使えるようになってきた言語の機能です。
注:この記事ではPromiseについての基本的なことしか扱いません。また、具体例については自身で試してもらう他、発表で用いたコードが僕のGitレポジトリにあるので、こちらをダウンロード、実行して、自分の予想通りに動くかを確かめると良いんじゃないかと思います。

Promiseとは

PromiseとはES6(ECMAScript2015)で採択されたもので、次のようなものです。

・主にコールバック地獄を解消するために作られた
・非同期処理を簡単に書けるようにしてくれるもの
・作成時点では値が確定していない値へのプロキシ
・コンストラクタとして使い、主としてはインスタンスを利用

async, awaitとは

async, awaitもやはりES6で採択されたものですが、Promiseを簡単に扱えるようにしてくれるものです。 Promiseが分からないと、async, awaitはまず理解できません。async, awaitを学習するのは、Promiseを理解してからにしましょう。

Promiseに入門する

promise(Promiseのインスタンス)は3状態あり、pending(未実行)、fulfilled(実行済み)、rejected(拒否済み)があります。 デフォルトはpendingで、fulfilledないしrejectedになったらもう変更は不可能です。 そして、3つの他に「自身の状態が参照先のpromiseの状態に固定(lock in)される」という特殊な状態があります。 とはいえ、参照先のpromiseの状態はやはりpending, fulfilled, rejectedしかないので、全てのpromiseは3状態のいずれかになります。

Promiseをコンストラクタとして呼ぶ際には、引数に関数を渡します。この関数はインスタンスの値が返る前に同期的に実行されます。この関数が実行される際は2引数(どちらも関数)が与えられ、それぞれresolve, rejectと呼びます。 これらの関数(resolve, reject)を呼ぶと、返るpromiseの状態はfulfilled, rejectedになります。(resolveを呼ぶとfulfilledになります。対応が面倒ですが、そう呼ぶことになっているので仕方ないです。)

例:

// pending状態のpromise
const pendingPromise = new Promise((resolve, reject) => {
  // 内部でresolveもrejectも呼んでいない
}); // => Promise {<pending>}

// 'fulfilled!'という値にてfulfilled状態のpromise
const fulfilledPromise = new Promise((resolve, reject) => {
  resolve('fulfilled!');
}); // => Promise {<resolved>: "fulfilled!"}

// 'rejected!'という値にてrejected状態のpromise
const rejectedPromise = new Promise((resolve, reject) => {
  reject('rejected!');
}); // => Promise {<rejected>: "rejected!"}

また上の例でわかる通り、fulfilledないしrejected状態のpromiseはその状態になる際に1つの引数を取ることができ、実は後から利用することができます。呼び方の話ですが、rejectedの場合その引数はreasonと呼ぶことが多いです。

Promiseを便利にしてくれるthen

Promise.prototype.thenに関数が定義されており、promiseはいつでもthenメソッドを呼ぶことが出来ます。
thenは関数1つまたは2つを引数に取り、同期的に新たにpendingなpromiseを生成して返します。この引数の関数は、呼び出し元のpromiseがある条件を満たすと非同期的に呼ばれます。

promise.then(onFulfilled[, onRejected]); // => Promise {<pending>}(これはthenを呼んだpromiseとは違う)

thenの引数の関数の実行タイミング

さて、thenの引数の関数はいつ呼ばれるのでしょうか。
答えは単純で、thenの引数関数のonFulfilledは、呼んだ元のpromiseがfulfilled状態になったタイミングで実行されます。
一方、onRejectedは呼んだ元のpromiseがrejected状態になったタイミングで実行されます。
onFulfilledが実行される際の引数はそのpromiseがfulfilledされた値、onRejectedの場合はそのpromiseがrejectedにされた値(reason)にて呼ばれます。

const promise1 = new Promise((resolve, reject) => {
  resolve({ a: 1, b:2 });
});

// ここ以降のthenが引数が1つだけで呼ばれている場合は、onRejectedはundefinedとして実行しています。
promise1.then((value) => {
  console.log(value.a + value.b); // => 3
});

const promise2 = new Promise((resolve, reject) => {
  reject({ a: 1, b:2 });
});

promise2.then(undefined, (value) => {
  console.log(value.a + value.b); // => 3
});

thenは、引数関数の実行が例外を発生することなく完了するとその返り値で、同期的に生成して返していたpromiseをfulfilled状態にします。

これを利用すると、次のようなことができます。

const promise = new Promise((resolve, reject) => {
  resolve({ a: 1, b:2 });
});

const promise1 = promise.then((value) => {
  return value.a + value.b; // 1 + 2
});

promise1.then((value) => {
  console.log(value * 2); // => 3 * 2
});

promiseでresolveする

また、今度はthenの引数の関数の返り値でpromise(returnedPromiseと呼ぶことにします)を返す場合です。 この場合、thenが同期的に返していたpromiseは、引数の関数の返り値のreturnedPromiseでfulfilledされることになります。 しかし、このようにresolveの引数がpromiseの場合(thenの返り値でfulfilledされる場合でも内部的にresolveが呼ばれています)、通常と異なる仕組みがはたらきます。 具体的にいうと、今回のようにpromiseがpromise(今回はreturnedPromise)を引数としてresolveされる場合、promiseはreturnedPromiseを参照しはじめ、自身の状態はreturnedPromiseの状態にlock inされることになります(ちょっと追いきれていないですが、多分このタイミングでlock inされているはずです)。

const promise = new Promise((resolve, reject) => {
  resolve({ a: 1, b:2 });
});

const promise1 = promise.then((value) => {
  return new Promise((resolve) => {
    resolve('fulfilled');
  });
});

promise1.then((value) => {
  // lock in状態では、「なにで」fulfilledされたかもやはり参照します。
  console.log(value); // => 'fulfilled'
  // valueにpromiseは入らない。
});

参照が深い場合も同様です(lock in先のpromiseがlock inされている場合、その先、その先へと見て行きます)。

// Promise.resolveは、最初から引数でfulfilled状態のpromiseを生成します。
Promise.resolve(Promise.resolve(Promise.resolve('fulfilled!'))).then((message) => {
  console.log(message); // => 'fulfilled!'
});

thenの引数の関数の実行中に例外が発生

次に、thenの引数の関数の実行中に例外が発生した場合です。 このとき、そのthenが同期的に生成していたpromiseは、その例外をreasonとしてrejected状態になります。

const promise = new Promise((resolve, reject) => {
  resolve({ a: 1, b:2 });
});

const promise1 = promise.then((value) => {
  throw new Error('err!!');
});

promise1.then((value) => {
  console.log(value); // 実行されない
}, (reason) => {
  console.log(reason.message); // => 'err!!'
});

thenチェーンとは

最後に、一般にthenチェーンと呼ばれるものを見てみましょう。 (promiseに対してthenを鎖のように連続して呼んでいる様を指して、thenチェーンなどと呼びます。よく使われる形です) 最初からこれを見ると分かりにくいのですが、上のようにthenごとに新しいpromiseが生成されていることを分かっていれば怖くありません。

const promise = Promise.resolve('fulfilled!');
promise.then((message) => {
  console.log(message); // => 'fulfilled!'
  return 2;
}).then((n) => {
  return n * 3; // => 2 * 3
}).then((n) => {
  console.log(n * 4); // => 6 * 4
});

おわりに

今回はかなり基礎的な部分の説明になってしまいましたが、どれも重要なことだと思います。 自分もまだまだpromiseに入門したばかりなので、これからもっと使いこなしていけるようになりたいです。あと、今回初めてECMAScriptの仕様をちゃんと読んだのですが、こういう風に書かれているのか...。と勉強になりました。async, awaitについてもいつか書く機会があれば書きたいと思います(あれば)。