コンパイラかく語りき

import { Fun } from 'programming'

今さら始めるJavaScript Promiseの基礎の基礎

最近ようやくPromiseを触り始めました。わりと雰囲気で使ってしまっているので、ここらで自分のために整理を…。 ただ、基本的な例がすでに複雑というか、初心者にとってはムズい気がするので、超噛み砕いてみました。

Promise - MDN

最小構成

Promiseオブジェクトを初期化します。引数の関数は必須です。

new Promise(() => {})

引数が空だと、以下のように怒られます。

new Promise()
TypeError: Promise resolver undefined is not a function

数値で誤魔化そうとしてもムダみたいです。

new Promise(10)
TypeError: Promise resolver 10 is not a function

関数の引数

Promise初期化時の関数には、引数を2つ渡せます。 第一引数が"成功"のための関数、第二引数が"失敗"のための関数です。

new Promise((seikou, sippai) => {})

成功させる

成功させます。第一引数の関数を実行すると"成功"となります。

new Promise((seikou, sippai) => {
  seikou()
})

失敗させる

失敗させます。第二引数の関数を実行すると"失敗"となります。

new Promise((seikou, sippai) => {
  sippai()
})

ちなみに、正式には成功した状態を"fulfilled"、失敗した状態を"rejected"と呼ぶらしいです。

後処理も書いてみる

後処理も書いてみます。

new Promise(() => {})
  .then(() => {}) // Promiseが"成功"ならthen以下が呼ばれる。
  .catch(() => {}) // Promiseが"失敗"ならcatch以下が呼ばれる。

確認

成功したことを確認してみましょう。

new Promise((seikou, sippai) => {
  seikou()
})
  .then(() => {
    console.log('成功したのでこっちが呼ばれる')
  })
  .catch(() => {})

一応、失敗したことも確認してみましょう。

new Promise((seikou, sippai) => {
    sippai()
  })
    .then(() => {})
    .catch(() => {
      console.log('失敗したのでこっちが呼ばれる')
    })

resolve, reject

一般的には、seikouにはresolve(解決), sippaiにはreject(拒絶)という名前が使われます。

new Promise((resolve, reject) => {})

Promiseを返す関数

Promiseを作って返す関数を作ってみます。

const createPromise = () => {
  return new Promise((resolve, reject) => {})
}

Promiseを成功させるかどうか。引数を渡して決めてみましょう。

const makePromise = (seikou) => {
  return new Promise((resolve, reject) => {
    if(seikou) {
      resolve() // 成功させる
    } else {
      reject() // 失敗させる
    }
  })
}

Promise(約束)を破ってみます。

const brokenPromise = makePromise(false) // falseを渡すとrejectが実行される
brokenPromise
  .then(() => {
    // 今回はこっちは呼ばれない
  })
  .catch(() => {
    // こっちが呼ばれる。
    console.log('失敗させたのでこっちが呼ばれる')
  })

成功/失敗の理由を持たせてみましょう。

まずは関数を定義して、

const promiseAgain = (seikou, riyuu) => {
  return new Promise((resolve, reject) => {
    if(seikou) {
      resolve(riyuu) // 成功させる
    } else {
      reject(riyuu) // 失敗させる
    }
  })
}

実行します。

const brokenPromiseAgain = promiseAgain(false, 'やる気が起きない')
brokenPromiseAgain
  .then((result) => {
    // 今回もこっちは呼ばれないけど、成功結果はresultとして渡される。
  })
  .catch((err) => {
    // rejectの理由がerrとして渡される
    console.log(`失敗理由は、${err}から。`)
  })

成功/失敗が確定しているなら

成功(resolve)だけを行うなら、以下のように書くことができます。

const keptPromise = () => {
  return Promise.resolve('必ず成功するPromise')
}

実行します。

keptPromise()
  .then((result) => {
    console.log(result) // '必ず成功するPromise'
  })
  .catch(() => {
    // こっちは実行されない。
  })

これは以下と同様です。

const keptPromise = () => {
  new Promise((resolve) => {
    resolve('必ず成功するPromise')
  })
}
keptPromise()
  .then((result) => {
    console.log(result) // '必ず成功するPromise'
  })
  .catch(() => {
    // こっちは実行されない。
  })

失敗(reject)だけも可能です。

const keptPromise = () => {
  return Promise.reject('必ず失敗するPromise...')
}

Promise.resolve() - MDN Promise.reject() - MDN

複数のPromiseの実行を待つ

全てが成功する場合

Promise.all関数を使えば、複数のPromiseを処理できます。 Promise.all自体はPromiseを返すのですが、引数にとった複数のPromiseがすべてresolveされるまで、thenされません。

まずは、Promiseを返す関数をいくつか定義します。setTimeout関数を使い、ちょっとずつ成功をずらしています。

const promiseA = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('PromiseAが成功')
      resolve()
    }, 2000) // 一番遅い
  })
}
const promiseB = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('PromiseBが成功')
      resolve()
    }, 1000)
  })
}
const promiseC = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('PromiseCが成功')
      resolve()
    }, 1500)
  })
}

次に、Promise.all関数を使います。複数のPromiseを配列の形式で指定し、全てが成功したらかかった時間を表示します。

const startAllPromises = () => {
  const startTime = new Date() // 計測開始時間
  Promise.all([ 
    promiseA(),
    promiseB(),
    promiseC()
  ]).then(() => {
    const endTime = new Date() // 計測終了時間
    console.log(`${(endTime - startTime) / 1000}秒で、全てのPromiseが成功`)
  })
}
startAllPromises() // だいたい2秒くらいが表示される

一番成功の遅いPromiseAが終わったら、Promise.allの返すPromiseのthenが呼ばれます。

ちなみに、ABCのどれかが失敗(reject)した場合は、Promise.all()のthenは実行されません。

失敗ケースを補足する場合

Promise.allのあとにcatchをチェーンすればOKです。

失敗するPromiseを新しく定義します。

const promiseD = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('PromiseDが失敗…')
      reject('D')
    }, 1700)
  })
}

失敗は1700ミリ秒後。PromiseCが成功する後、PromiseAが成功する前に失敗します。

const onePromiseFails = () => {
  const startTime = new Date()
  Promise.all([
    promiseA(),
    promiseB(),
    promiseC(),
    promiseD() // ← 失敗するPromiseを追加
  ]).catch((err) => {
    const endTime = new Date()
    console.log(`${(endTime - startTime) / 1000}秒で、promise${err}が失敗したので終了`)
  })
}
onePromiseFails()

これで失敗の補足はできましたが、すでに走り出したPromiseが停止するわけではないようです。

コンソール出力。

PromiseBが成功
PromiseCが成功
PromiseDが失敗…
1.703秒で、promiseDが失敗
PromiseAが成功

Promise.allのthenが呼ばれないだけで、各Promiseはきちんと実行されるんですね。

Promise.all() - MDN

基礎の基礎だけですが、以上です。 何か間違いなどありましたら、ご指摘いただけると幸いです。

素晴らしい資料

JavaScript Promiseの本 言わずと知れた、AzuさんのPromise解説。