0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【定期】Promiseを理解したい

Posted at

これは何?

先達の御多聞に漏れず、JavaScriptのPromiseがなかなか理解できないので学習アウトプットを残します。

学習リソース

前提知識

  • 関数基礎
  • oop基礎
  • 同期/非同期処理 の概要
  • コールバック地獄

Promiseとは

非同期的な処理を同期的に行おうとする際、コールバック式でのコーディングではネストがどんどん深くなる。
フラットに書けるように組み込みのオブジェクトとしてPromiseが提供されている。

文字通り「約束ごとと、それが完了した結果」 を 表現したもの、と個人的には捉えている。

コンストラクタ

コンストラクタは下記の通り:

const promise = new Promise(function(resolve, reject) {
  // ... 任意の実行コード
});
  • コンストラクタは引数に1つのコールバック関数 : 通称executor を取る
  • executor はさらにコールバック関数2つ : resolvereject を引数に取る
  • resolve と reject は JavaScriptエンジン が提供する
    • コーダーはその実装を意識する必要はない
    • その動作は理解する必要がある
  • executor は resolve と reject のどちらかを内部でコールしないといけない

Promiseオブジェクトのプロパティ と resolve/reject の関係

Promiseは状態(state)と結果(result)を所持している。

  • state は pending / fulfilled / rejected のいずれか。初期値はpending
    • fulfilled / rejected の両方を指してsettled(解決済み)とも呼称する
  • result は resolve/reject の引数に渡した値がそのまま渡る。初期値はundefined

executor内部でresolve("hoge")とコールするとPromiseインスタンスは下記のように変わる。

  • state は fulfilled
  • result は hoge

また、executor内部でreject("fuga")とコールするとPromiseインスタンスは下記のように変わる。

  • state は rejected
  • result は fuga

ただし、javascript.infoでは、rejectの引数にはErrorオブジェクトを渡すことを推奨している。

技術的には、任意の型の引数で reject を呼び出すことが可能です(resolve のように)。しかし、reject (またはそれを継承したもの)では、Error オブジェクトを利用することを推奨します。

stateresult は内部的な値であり、通常これらに直接アクセスすることはしない。
ただし後述の重要なthenメソッドの振る舞いに大きく関わってくる。

then関連メソッド

基本構文は下記の通り :

promise.then(
  function(result) { /* 成功した結果を扱う */ },
  function(error) { /* エラーを扱う */ }
);

自身のstatusがfulfilledになった場合...すなわちexecutorの内部でresolveがコールされた場合、第一引数の関数が実行される。
自身のstatusがrejectedになった場合...すなわちexecutorの内部でrejectがコールされた場合、第二引数の関数が実行される。

正常終了にのみ関心があるなら引数は一つで良い :

promise.then(
  function(result) { /* 成功した結果を扱う */ }
);

異常終了にのみ関心があるなら第一引数をnullにすれば良いが、短文表現としてcatchが用意されている :

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// .catch(f) は promise.then(null, f) と同じです
promise.catch(alert); // 1秒後に "Error: Whoops!" を表示

fulfilled/rejectどちらでも構わないのでとにかくsettledに解決されたとき発火したいならfinallyが使える :

new Promise((resolve, reject) => {
  /* 時間のかかる処理を行い、その後 resolve/reject を呼び出す */
})
  // 成功か失敗かは関係なく、promise が確定したときに実行されます
  .finally(() => 読込中のインジケータを停止する )
  // したがって、読み込み中のインジケータは結果/エラーを処理する前に必ず停止されます
  .then(result => 結果を表示する, err => エラーを表示する)

then の戻り値

Promise.then(callback)は新たなPromiseインスタンスを返す。
ただし、new した時とは違い、ここで返ってくるPromiseインスタンスのstatependingresultundefinedとは限らない。
callbackの戻り値によって、then(callback)の戻り値のPromiseインスタンスの状態は次のように決定される。

callbackの戻り値 thenが返すPromiseの挙動
プリミティブ値、オブジェクトなど その値で即resolve
何も返さない undefined で即resolve
Promise 戻り値のPromiseの完了を待ち、その結果でresolve/reject
例外 rejectされる

この性質を利用して、複数の非同期処理を手続き的に書く手法がPromiseチェーンである。

Promiseチェーン

Promiseチェーンの例 :

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

  alert(result); // 1
  return result * 2;

}).then(function(result) { // (***)

  alert(result); // 2
  return result * 2;

}).then(function(result) {

  alert(result); // 4
  return result * 2;

});

個人的に、この例ではthen内部のコールバックで明示のreject/resolveはいらないの?というのが躓きポイントだったが、先述の表の通り、return により暗黙でresolveされるため、次段のthenもそれによって発火する...という流れになる、と理解した。

Promiseチェーンのグッドパターン

拡張性確保のための良いプラクティスとして、thenのコールバックはPromiseを返すことが推奨されている。
チェーンの中にアロー関数を含めると大変見づらくなるので切り出す方がよい。

例 :

// 商品の注文を...
// 受注 ➡ 発送 ➡ 通知 するPromiseチェーンの例
// 各々の作業の完了の後、次の作業が始まるが、たまにミスる模様

// 英語がガバかったらご容赦...

orderRecieve("mikan")
.then(deliverly)
.then(notifySucceed)
.catch(err=>console.log(err))

function orderRecieve(itemName) {
  return new Promise((resolve)=>{
    setTimeout(
      () => {
        console.log(`order recieved : ${itemName}`)
        resolve(itemName)
      }
      ,1000
    )}
  )
}

function deliverly(itemName){
  return new Promise((resolve,reject)=>{
    // たまにミスる
    if (Math.random() > 0.499) {
      reject(new Error("Oops! I have eaten it!!"))
    } else {
      setTimeout(
        () => {
          console.log(`delivered : ${itemName}`)
          resolve(itemName)
        }
        ,5000
      )
    }
  })
}

function notifySucceed(){
  return new Promise((resolve)=>{
    setTimeout(
      () => {
        console.log(`Succeed`)
        resolve()
      }
      ,5000
    )
  })
}

一旦ここまで

どうにかこうにか腑に落ちた...と思うのでasync/awaitのお勉強がまだですが一旦切り上げます。
苦戦したら続きとして投稿させていただきます。

誤りがあればご指摘など頂けると幸甚です。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?