技術メモなど

業務や日々のプログラミングのなかで気になったことをメモしています。PHP 成分多め。

JavaScriptのPromiseについて調べた

JavaScript再入門中async/awaitを覚えるにあたってPromiseをおさらいしないといけなさそうだったのでメモ。

Promiseとは

Promiseは非同期処理の最終的な完了もしくは失敗を表すオブジェクトです。 Promiseを使う - MDN

// 構文
new Promise( function(resolve, reject) { ... } );

Promiseの状態

生成されたPromiseオブジェクトは、以下の状態を持つ。

  • pending: 初期状態。成功も失敗もしていない。
  • fulfilled: 処理が成功して完了した。
  • rejected: 処理が失敗した。

Promiseコンストラクタに与えられた関数(executor関数)には、2つの引数resolve関数とreject関数が渡される。
executor関数は内部でなんらかの非同期処理を実行し、 成功した場合にはresolve関数を呼び出すことでfulfilledに、 失敗した場合にはreject関数を呼び出すことでrejectedPromiseオブジェクトの状態を変更できる。
また、executor関数で例外がスローされた場合、そのPromiseオブジェクトの状態はrejectedとなる。

var promise1 = new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", 'http://example.com');
    xhr.onload = () => resolve(xhr.status); // resolveは成功を表す
    xhr.onerror = () => reject(xhr.status); // rejectは失敗を表す
    xhr.send();
});


var promise2 = new Promise((resolve, reject) => {
    throw new Error('error!'); // 例外がスローされた場合はrejected
});

then / catch / finally

生成したPromiseオブジェクトは、インスタンスメソッドthenを使って成功時/失敗時の処理を登録することができる。
Promiseオブジェクトが成功(fulfilled)時には第一引数の関数が、失敗(rejected)時には第二引数の関数が実行される。
また、thenの他に、失敗時の処理のみを登録できるcatchメソッド、Promiseオブジェクトの成功/失敗に関わらず必ず実行されるfinallyメソッドがある。

promise.then(
    function(value){ 
        /*
            成功時の処理
            引数valueには、reslve()に渡された値が入っている
        */ 
    }, 
    function(reason){
        /*
            失敗時時の処理
            引数reasonには、reject()またはError()に渡された値が入っている
        */
    }
);

promise.catch( function(reason){ /* 失敗時時の処理 */} );

//catchを使った書き方は、以下の書き方と等価
promise.then(
    undefind,
    function(reason){ /* 失敗時時の処理 */}
);

promise.finally(function(){
    /* 
        成功/失敗共通の処理
        finallyメソッドは引数をとらない。
    */
});

Promiseチェーン

実行されたthen/catch/finallyメソッドは、新しくPromiseオブジェクトを返す。 そのため、メソッドチェーンのように書くことができる。

var promise = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve(0);
  }, 300);
});

promise
  .then( value =>{
    console.log(value);
    // コールバック関数内でreturnすることで、値を後続の処理に引き渡せる。
    return ++value;
  })
  .then( value =>{
    console.log(value);
    return ++value;
  })
  .then( value =>{
    console.log(value);
    return ++value;
  })
;
// 出力は以下のようになる
// 0
// 1
// 2

なお、メソッドチェーンの途中で例外が発生した場合、その処理は中断され、後続の最も近いcatchメソッドもしくはthenのエラー処理関数が実行される。

var promise = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve(0);
  }, 300);
});

// catchによる例外処理
promise
  .then( value =>{
    console.log(value);
    return ++value;
  })
  .then( value =>{
    // 例外をスロー
    throw new Error('Error!');
  })
  .then( value =>{
    // 前の処理で例外がスローされているため、この処理は実行されない
    console.log(value);
    return ++value;
  })
  .catch(error => {
    console.log(error);
  })
;
// 出力は以下のようになる
// 0
// Error: Error!
var promise = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve(0);
  }, 300);
});

// thenのエラー処理関数による例外処理
promise
  .then( value =>{
    console.log(value);
    return ++value;
  })
  .then( value =>{
    // 例外をスロー
    throw new Error('Error!');
  },
  error =>{
    // 同じthenメソッドに登録されているエラー処理関数は実行されない。
    console.log(`then1:  ${error}`);
  })
  .then( value =>{
    // 前の処理で例外がスローされているため、この処理は実行されない
    console.log(value);
    return ++value;
  },
  error =>{
    console.log(`then2:  ${error}`);
  })
  .catch(error => {
    // 前処理のthenで例外が処理されているため、この処理は実行されない。
    console.log(`catch:  ${error}`);
  })
;
// 出力は以下のようになる
// 0
// then:  Error: Error!

Promise.resolve と Promise.reject

Promiseオブジェクトの生成にはnew Promise()の他にもシンタックスシュガーとしてPromise.resolveおよびPromise.rejectという静的メソッドが用意されている。

Promise.resolveは成功状態の、Promise.rejectは失敗状態のPromiseオブジェクトを返却する。

Promise.resolve('success');

// 上記は以下と等価
new Promise(function(resolve){
    resolve('success');
});

Promise.reject(new Error('error!'));

// 上記は以下と等価
new Promise(function(resolve,reject){
    reject(new Error('error!'));
});

Promise.all と Promise.race

複数のPromiseオブジェクトをまとめて処理する方法として、Promise.allPromise.raceがある。

Promise.allは引数にPromiseオブジェクトの配列を取り、すべてのオブジェクトが成功状態のとき後続のthenの成功時処理を実行する。ひとつでも失敗するとthenは実行されず、catchまたはthenの失敗処理が実行される。

Promise.all([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3),
])
.then(values =>{
  console.log(values[0]);
  console.log(values[1]);
  console.log(values[2]);
});
// 出力は以下
// 1
// 2
// 3

Promise.all([
  Promise.resolve(1),
  Promise.reject(new Error('error')),
  Promise.resolve(3),
])
.then(values =>{
  console.log(values[0]);
  console.log(values[1]);
  console.log(values[2]);
})
.catch(error =>{
  console.log(error);
});
// 出力は以下
// Error: error

Promise.raceもまた引数にPromiseオブジェクトの配列を取り、どれか一つでも成功または失敗の状態になったら後続の処理を実行する。

var promise = Promise.race([
    new Promise(function (resolve, reject) {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", 'http://example.com');
        xhr.onload = () => resolve(xhr.status);
        xhr.onerror = () => reject(xhr.status);
        xhr.send();
    }),
    new Promise(function (resolve, reject) {
         setTimeout(() => reject(new Error('request timeout')), 5000)
    })
]);
      
promise
  .then(value => console.log(value))
  .catch(error => console.log(error));

なお、後続の処理に引き渡されるのは最初に解決したPromiseオブジェクトの結果のみだが、他の処理が中断されるわけではないので注意が必要。

var promise = Promise.race([
    new Promise(function (resolve, reject) {
        console.log('promise 1');
        setTimeout(() => reject(new Error('request timeout')), 5000)
    }),
    new Promise(function (resolve, reject) {
        console.log('promise 2');
        setTimeout(() => reject(new Error('request timeout')), 1000)
    })
]);
      
promise
  .then(value => console.log(value))
  .catch(error => console.log(error));
// 出力は以下
// promise 1
// promise 2
// Error: request timeout

記事中のソースは以下にまとめてあります。

Promise調査 · GitHub

参考文献

Promiseを使う - JavaScript | MDN

Promise - JavaScript | MDN

JavaScript Promiseの本