【JavaScript】Promiseの使い方をまとめてみた。

JavaScript

はじめに

Promiseが登場する以前はコーバック地獄非同期処理は実行順序を制御できない問題がありました。

Promiseはこれらの問題を解決するために考案されました。

Promiseとは

Promiseは非同期処理の状態(完了もしくは失敗の結果)およびその結果の値を表します。

Promiseの状態は待機(pending)、完了 (fulfilled)、失敗 (rejected)の3種類の状態があります。

処理が完了したらfulfilled、失敗したらrejected、完了も失敗もしていない時はpending状態です。

Promiseの使い方

まず下記のコードを見てください。Promiseオブジェクトを作成しています。

let promise = new Promise((resolve, reject)=> { // ここに処理を書く });

Promiseの引数には関数(コールバック関数)を渡し、第一引数にresolve、第二引数にreject(任意)を設定します。

resolve関数が呼ばれたら、処理が正常に完了したことを示し、reject関数が呼ばれたらエラーが発生(失敗)したことを示します。

いまいちわからないと思うので、次を見てください。

Promiseの状態について

待機( pending )

まず、処理を記載してないpromise(初期状態)は以下の値となります。

Promise {<pending>}
  [[Prototype]]: Promise
  [[PromiseState]]: "pending"
  [[PromiseResult]]: undefined

PromiseState : "pending"は待機、つまり完了も失敗もしていない初期状態です。

PromiseResult : undefinedは処理の計算結果です。初期値は undefined です。

完了( fulfilled )

次に、PromiseStatefulfilledにしてみましょう。

let promise = new Promise((resolve, reject)=> {
  resolve('成功したよ');
})

処理にresolve(value)を呼ぶことで、状態をfulfilledに、PromiseResultの値はvalueで設定できます。

Promise {<fulfilled>: '完了したよ'}
  [[Prototype]]: Promise
  [[PromiseState]]: "fulfilled"
  [[PromiseResult]]: "成功したよ"

PromiseStatefulfilledに、PromiseResult成功したよに変わっているのがわかります。

失敗( rejected )

同じ要領で、PromiseStaterejectedにしてみましょう。

let promise = new Promise((resolve, reject)=> {
  reject(new Error('失敗しちゃった'));
})

処理にreject(error)を呼ぶことで、状態をrejectedに、PromiseResultの値はerrorで設定できます。

rejecterrorにはError オブジェクトを利用するのが一般的です。

Promise {<rejected>: Error: 失敗しちゃった
  [[Prototype]]: Promise
  [[PromiseState]]: "rejected"
  [[PromiseResult]]: Error: 失敗しちゃった

ちゃんとPromiseStaterejectedに、PromiseResultError:"失敗しちゃった"になっていますね。


Promise の状態がpendingからfulfilledあるいはrejectedになった後、その状態を変更することはできません。
この後説明する、then()catch()メソッドは呼び出すたびに新しいPromiseオブジェクトを返しています。
PromiseStatePromiseResult は内部プロパティです。
内部プロパティは直接アクセスすることができないので、then()catch()メソッドを用いてアクセスします。

これらの3種類の状態とPromiseチェーンを使用して、一連の非同期処理をスマートに表現していきます。

Promiseチェーン

Promiseの最も重要な利点の一つが、一連の非同期処理を、連続するthen()catch()finally()メソッドの呼び出しとして表現できることです。

これらのメソッドは全てPromiseオブジェクトを返します。なので、処理を入れ子にする必要がなく、チェーン(連鎖)させることができます。

fetch()     // fetchメソッドはPromiseオブジェクトを返します
.then()     
.then()
.then()
.catch()
.finally()

メソッドチェインと同じ原理なので、解説はこちらを参考にしてください。

then()

then()は最も基本的で重要なメソッドです。

下記のコードを見てください。

let promise = new Promise((resolve, reject)=> {
  resolve('完了したよ');
}).then(
  result => console.log(result), // promiseの状態がfulfilledの場合、実行
  error => console.log(error)    // promiseの状態がrejectedの場合、実行
)

Promiseオブジェクトにthen( function(result), function(error) )を繋げて、新たな処理を実行しています。


then()はPromiseの状態がfulfilledrejectedになるまで、実行されません。

then()
1. 第一引数の関数は、Promise が完了(fulfilled)されたときに実行され、その結果を受け取ります。
2. 第二引数の関数は、promise が失敗(rejected)されたときに実行され、エラーを受け取ります。(完了の場合だけを扱いたい場合、第一引数だけ指定すれば良い)

第一引数の関数が呼び出されたとき、Promiseが満たされたと言い、
第二引数の関数が呼び出されたとき、Promiseが失敗したと言います。

またコールバック関数の引数(resulterror)には、PromiseオブジェクトのPromiseResultの値を受け取ることができます。(今回は'完了したよ'という文字列)


上記のコードではPromiseの状態がfulfilled なので、result => console.log(result)が実行され、コンソールに完了したよと表示されます。

Promiseオブジェクトが失敗(rejected)の場合は第二引数の関数が実行されます。

Promiseが完了か失敗の状態によって、実行する処理を制御できるのがthen()メソッドです。

catch()

catch()then()のエラーにだけ特化したようなメソッドです。

then()は第一引数にnullを設定でき、エラーの場合のみ実行させたい場合、then(null, function(error))のような書き方ができます。

then(null, function(error))catch(function(error))は同じ動きをします。

let promise = new Promise((resolve, reject)=> {
  reject(new Error('失敗しちゃった'));
}).catch(
  error => console.log(error) // 前のpromiseの状態がrejectの場合、実行
)
// 実行後、コンソールにerrorが表示される
Error: 失敗しちゃった
  at <anonymous>:2:10
  at new Promise (<anonymous>)
  at <anonymous>:1:15

catch()then(null, function(error))を簡略化したものです。一般的にcatch()を使います。

finally

例外処理で try {...} catch {...} に finally 節があるように、Promise にも finally() があります。

finnally()はPromiseが完了、失敗どちらの状態であっても、後始末(開いたファイルを閉じる、ネットワークの接続を閉じるなど)をする必要がある際に最適なメソッドです。

let promise = new Promise((resolve, reject)=> {
  reject(new Error('失敗しちゃった'));
}).catch(
  error => console.log(error) // 前のpromiseの状態がrejectの場合、実行
).finally(()=> {
  console.log('ファイナリーー') // promiseの状態がfulfilledかrejectのどちらの場合でも、実行
})

finally()の特徴として、
1. 引数は渡されません。なので、完了(fulfilled)か失敗(rejected)の判断ができません。
2. finally()の結果はthen()catch()に引数として渡すことができます。


一連の流れをまとめたものです。

MDN Web Docsのpromise解説ページから借りてきました。https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise

then()catch()は新しいPromiseオブジェクトを返します。
then()catch()にコールバック関数を引数として渡した時にthen()は、新しいPromiseを返します。
このPromiseはまだpending状態です。
後で、非同期にthen()のコールバック関数がなんらかの処理を行い、返値を返します。
Promiseはその返値によって解決されます。(resolve)
返値が値もしくは何も返さなかった場合は、Promiseはすぐに満たされます。

これの繰り返しです。


ここで、then()の完了時と失敗時の処理後のPromiseの値を見てみましょう。

let promise = new Promise((resolve, reject)=> {
  resolve('完了したよ');
}).then(
  result => console.log(result), // 前のpromiseの状態がfulfilledの場合、実行
)
let promise = new Promise((resolve, reject)=> {
  reject(new Error('失敗しちゃった'));
}).then(null,
  error => console.log(error) // 前のpromiseの状態がrejectの場合、実行
)
// then(result => console.log(result))実行後のPromise
Promise {<fulfilled>: undefined}
  [[Prototype]]: Promise
  [[PromiseState]]: "fulfilled"
  [[PromiseResult]]: undefined

// then(error => console.log(error))実行後のPromise
Promise {<fulfilled>: undefined}
  [[Prototype]]: Promise
  [[PromiseState]]: "fulfilled"
  [[PromiseResult]]: undefined

どちらのPromiseも同じ状態(fulfilled)になっています。

なぜ?と思った人はthen()の認識が違っています。

then()メソッドは単にコールバック関数を登録するメソッドと思ってください。
addEventListenerと同じようにイベントが発生したら、関数を実行するようなものです。

then()の場合はPromiseの状態によって、渡されたコールバック関数を実行するだけです。

Promiseがrejectedの状態になるのは処理中にエラーや例外が発生して、拒否された場合です。

逆に、then()のコールバック関数の処理が無事終了し、返り値が通常の値、もしくは何も返さない時は、fulfilledの状態になり、返り値がPromiseResultにセットされます。(何も返さない時はundefined)

上記の例はただコンソールにerrorを表示して、返り値に何も返していないので、
State:fulfilledResult:undefinedです。

Promise.prototype.then() - JavaScript | MDN
then() メソッドは Promise を返します。最大 2 つの引数として、 Promise が成功した場合と失敗した場合のコールバック関数を取ります。

async関数/await式

非同期的コードは、通常の同期的コードと違って、値を返したり、例外をスロー(throw文)したりできません。
満たされたPromise値は、同期的コードの戻り値のようなものです。

しかし、場合によってはコードが冗長になってしまいます。

async関数/await式を使用すれば、Promiseをより簡潔に書くことができます。

await式

awaitはPromiseを受け取り、戻り値やスロー(throw文)された例外に変換します。
また、Promiseが完了するまで、何も実行しません。

let promise = new Promise((resolve, reject)=> {
  resolve('成功したよ');
})

// PromiseをPromiseResultの値に変換している
await promise
/=> '成功したよ'


let promise = new Promise((resolve, reject)=> {
  reject(new Error('失敗しちゃった'));
})

// PromiseをPromiseResultの値に変換している(Uncaught errorが起きるが、、)
await promise
/=> Error: 失敗しちゃった

awaitを使うコードはそのコード自身も非同期になります。

またawaitを使えるのは、asyncを使って宣言された関数の中だけです。
上記のコードは開発者コンソールでは使用できますが、通常はasyncを宣言しないとawaitは使用できません。

async関数

先ほどのコードをasyncを使用して書き直したコードです。

async function getPromise() {
  let promise = new Promise((resolve, reject)=> {
    resolve('成功したよ');
  })

  // PromiseをPromiseResultの値に変換している
  await promise
  /=> '成功したよ'
}

関数にasyncを宣言するとその関数の戻り値はPromiseになります。


通常の関数で await を使うことはできません
非async関数で await を使おうとした場合、構文エラーになります。
await はトップレベルのコードでは動作しません。
このような書き方ではエラーが起きます。
let response = await fetch(URL);

async関数でラップしてやりましょう。

まとめ

JavaScriptの非同期通信を理解する上ではとても重要な要素なので、しっかり理解しておいたほうが良いです。

MDN Web Docsではちゃんとした例で説明しているので、わかりやすいです。
参考にしてください。

プロミスの使用 - JavaScript | MDN
プロミス (Promise) は、非同期処理の最終的な完了もしくは失敗を表すオブジェクトです。多くの人々は既存の用意されたプロミスを使うことになるため、このガイドでは、プロミスの作成方法の前に、関数が返すプロミスの使い方から説明します。

またこの記事もわかりやすいです。

前置き: コールバック

コメント

タイトルとURLをコピーしました