Promiseを利用して非同期処理を制御

JavascriptのPromiseについてご紹介します。「なぜPromiseを利用するのか?」「Promise状態の変更(resolve, reject)」「処理状態に応じたハンドリング方法(then, catch)」といったことを解説します。

Promiseを利用するメリット

Promiseを利用すると、非同期処理のコールバック地獄を回避することができます。

非同期処理とは?

時間の掛かる処理の完了を待たずに(同期せずに)、次の処理に進むことを非同期処理と言います。

const print = setTimeout(() => console.log('done'), 1000)

console.log('start')

// 非同期処理
print

console.log('end')
start
end
done

Promiseでコールバック地獄を回避

Promiseを 利用しなかった場合利用した場合 を比べてみてみます。

利用しない場合

ネストが深くなり、読みづらいです。

/**
 * @param {String} word
 * @param {String} addWord
 * @param {Function} callback
 * @returns {number}
 */
const addString = (word, addWord, callback) => setTimeout(() => {
    console.log(`${new Date() - startTime}ms`)
    const data = word ? `${word}_${addWord}` : addWord
    callback(data)
  },
  1000
)

const startTime = new Date()
addString('', 'aaa', data => {
  addString(data, 'bbb', data => {
    addString(data, 'ccc', data => {
      console.log(data)
    })
  })
})
1003ms
2009ms
3014ms
aaa_bbb_ccc

利用した場合

メソッドチェーンを利用して、ネストが深くなるのを防ぐことができます。

/**
 * @param {String} word
 * @param {String} addWord
 * @returns {Promise<any>}
 */
const addString = (word, addWord) => {
  return new Promise((resolve, reject) => {
    const timeoutFunction = data => {
      console.log(`${new Date() - startTime}ms`)
      return resolve(data)
    }

    setTimeout(
      () => word ? timeoutFunction(`${word}_${addWord}`) : timeoutFunction(addWord),
      1000
    )
  })
}

const startTime = new Date()
addString('', 'aaa')
  .then(data => addString(data, 'bbb'))
  .then(data => addString(data, 'ccc'))
  .then(data => console.log(data))
1004ms
2009ms
3013ms
aaa_bbb_ccc

処理内部でPromise状態を変更

実行した処理が返すPromiseの 状態 を利用して、処理ハンドリングが行われます。
「どういった状態があるのか」「状態をどのように変更するのか」を確認します。

Promiseの状態

状態 安定 説明
pending × 初期状態。操作未完了
fulfilled 操作完了
rejected 操作失敗

状態の変更

resolve関数 を利用すると fulfilled の状態になります。
reject関数 を利用すると rejected の状態になります。

const sample_promise = x => {
  return new Promise((resolve, reject) => {
    x ? resolve('成功') : reject('失敗')
  })
}

console.log('操作成功')
console.log(sample_promise(true))
console.log('------------------')
console.log('操作失敗')
console.log(sample_promise(false))
操作成功
Promise { '成功' }
------------------
操作失敗
Promise { <rejected> '失敗' }
(node:1820) UnhandledPromiseRejectionWarning: 失敗
(node:1820) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:1820) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

失敗したとき、ハンドリングされていないというメッセージが表示されています。

Promise状態に応じてハンドリング

関数が返すPromiseの状態(成功 or 失敗)に応じて、ハンドリングする方法を確認します。

const sample_promise = x => {
  return new Promise((resolve, reject) => {
    x ? resolve('成功') : reject('失敗')
  })
}

// 成功時呼ばれる関数
onFulfilled = data => {
  console.log('成功しました')
  console.log(data)
}

// 失敗時呼ばれる関数
onRejected = err => {
  console.log('失敗しました')
  console.log(err)
}

thenメソッド

第1引数で成功時の動作をハンドリング

sample_promise(true)
  .then(onFulfilled, onRejected)
成功しました
成功

第2引数で失敗時の動作をハンドリング

sample_promise(false)
  .then(onFulfilled, onRejected)
失敗しました
失敗

catchメソッド

catchメソッドでも、失敗時の動作をハンドリングできます。
さらに、catchメソッドを利用すると、thenの第1引数で失敗した場合もハンドリングすることができます。

// 成功時呼ばれる関数
onFulfilled = data => {
  console.log('成功しました')
  throw new Error()
}

// 失敗時呼ばれる関数
onRejected = err => {
  console.log('失敗しました')
  console.log(err)
}

onFulfilledのエラーをハンドリングできてない

sample_promise(true)
  .then(onFulfilled, onRejected)
成功しました
(node:2765) UnhandledPromiseRejectionWarning: Error

onFulfilledのエラーをハンドリングできている

sample_promise(true)
  .then(onFulfilled, onRejected)
  .catch(onRejected)
成功しました
失敗しました
Error

finalyメソッド

ES2017時点では存在しません。

非同期処理の同時実行

Promise.all

複数の非同期処理を同時に実行して、全て完了したらthenメソッドが実行されます。

const sample_promise = (word, time) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log(word)
        return resolve(word)
      },
      time
    )
  })
}

Promise.all([
  sample_promise('aaa', 1000),
  sample_promise('bbb', 500),
  sample_promise('ccc', 2000)
])
  .then(data => {
    console.log(data)
  })
bbb
aaa
ccc
[ 'aaa', 'bbb', 'ccc' ]

参考
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

Promise.race

複数の非同期処理を同時に実行して、1つ成功した時点ですぐにthenメソッドが実行されます。

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

Promise.race([
  sample_promise('aaa'),
  sample_promise('bbb'),
  sample_promise('ccc')
])
  .then(data => {
    console.log(data)
  })
aaa

参考
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/race