30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Promiseによる非同期処理の書き方

JavaScriptは基本的に逐次処理、同期処理であり、上から順番にプログラムが実行されていく。 x行目の処理が終わってからx+1行目の処理を行う、という具合に、一つ一つ実行していく。

非同期処理を行うための方法も以前から用意されていたが、ES2015で導入されたPromiseによって、より簡便に実装できるようになった。

なお、ここに書かれているものは全て、v6.4.0のNode.jsで実行した結果である。他の環境だと、ログの内容が異なるかもしれない。

Promiseの基本

new Promise()Promiseインスタンスを作り、それを操作することで非同期処理を管理していく。

new Promiseの引数には関数を渡す。
そしてその関数の第一引数としてresolveを、第二引数としてrejectを設定し、処理が上手くいったら前者を、失敗したら後者を実行する。
不要であれば、rejectは設定しなくてもいい。

つまり、以下のような形になる。

new Promise((resolve, reject)=>{

        // 非同期で行う何らかの処理

        // 処理が上手くいった場合
        resolve();

        // 処理が上手くいかなかった場合
        reject();

    };
});

そして、このインスタンスを返り値とする関数を作ることで、非同期処理を行う。

function myFunc(arg){
    return new Promise((resolve, reject)=>{
        if(arg === true){
            resolve('ok!');
        } else {
            reject('ng!');
        };
    });
};

// 以下の順番で出力される
// start!
// ok!
// ng!

myFunc(true).then((received)=>{
    console.log(received);
},(received)=>{
    console.log(received);
});

myFunc('hoge').then((received)=>{
    console.log(received);
},(received)=>{
    console.log(received);
});

console.log('start!');

上記では、myFunc()に渡された引数がtrueならresolve()を、それ以外ならreject()を実行している。
そして後述するthen()を使って、所定の処理を行わせている。

また、文末にあるstart!が最初に出力されていることから、myFunc()での処理は非同期で行われていることが分かる。
つまり、そこでの処理が終わるのを待つことなくコードを実行していく。

以下で、一つずつ解説していく。

promiseオブジェクトが持つ「状態」

promiseオブジェクト(new Promise()で作られたPromiseインスタンス)は必ず状態を持ち、それは以下の3つのいずれかである。

  • Pending(初期状態)
  • Fulfilled
  • Rejected

作成されたpromiseオブジェクトはまず、Pendeingという状態になっている。
この「状態」は、[[PromiseStatus]]という内部属性で定められているが、開発者がこれに直接アクセスする方法はない。

状態を変更する唯一の方法が、先程触れたresolve()reject()である。
resolve()を行うとFulfilledになり、reject()を行うとRejectedになる。

つまり、先程のmyFunc()の例で言えば、myFunc(true)を実行するとFulfilledのpromiseオブジェクトが返され、myFunc('hoge')を実行するとRejectedのpromiseオブジェクトが返される。

then

promiseオブジェクトは、then()メソッドを持つ。

function myFunc(arg){
    return new Promise((resolve, reject)=>{
        if(arg === true){
            resolve('ok!');
        } else {
            reject('ng!');
        };
    });
};

const promiseObj1 = myFunc(true);
console.log('then' in promiseObj1);  // true
console.log(typeof promiseObj1.then)  // function

const promiseObj2 = myFunc('hoge');
console.log('then' in promiseObj2);  // true
console.log(typeof promiseObj2.then)  // function

then()には引数として関数を渡すが、この第一引数と第二引数はそれぞれ、onFulfilledonRejectedと呼ぶのが一般的となっているようだ。
つまりthen(onFulfilled,onRejected)という形になる。onRejectedは省略できる。

そして、名前から推測できると思うが、promiseオブジェクトの状態がFulfilledになればonFulfilledが、RejectedになればonRejectedが、実行される。

function onFulfilled(arg){
    console.log('10未満です。');
    console.log('渡された数値: '+arg);
};
function onRejected(arg){
    console.log('10以上です。');
    console.log('渡された数値: '+arg);
};

function myFunc(num){
    return new Promise((resolve, reject)=>{
        // resolveやrejectの引数に渡したものが、そのままonFulfilledやonRejectedに渡される
        (num < 10) ? resolve(num) : reject(num) ;
    });
};

myFunc(3).then(onFulfilled, onRejected);
// 10未満です。
// 渡された数値: 3

myFunc(10).then(onFulfilled, onRejected);
// 10以上です。
// 渡された数値: 10

なお、reject()の引数には、Errorオブジェクトを渡すのが一般的らしい。

(num < 10) ? resolve(num) : reject(num) ;
// ↓
(num < 10) ? resolve(num) : reject(new Error(num)) ;

状態変化は一度きり

一度状態が変化したpromiseオブジェクトは、そのまま固定され、再び状態が変化するということはない。

let x = 0;
function myFucn(){
    return new Promise((resolve, reject)=>{
        resolve('hoge');
        reject('fuga');
        reject(x++);
    });
};

function onFulfilled(){
    console.log('このオブジェクトはfulfilledです。');
};

let obj = myFucn();

setTimeout(()=>{
    console.log(x); // 1
    console.log(obj);   // Promise { 'hoge' }
}, 0);

setTimeout(()=>{
    console.log(x); // 1
    console.log(obj);   // Promise { 'hoge' }
    obj.then(onFulfilled);  // このオブジェクトはfulfilledです。
}, 100);

上記ではresolve()のあとにreject()を実行しているが、promiseオブジェクトの状態はFulfilledのままである。
ただ、x1になっていることから分かるように、reject()に渡された処理そのものは、実行される。

また、仕様では、状態は必ず変化するのが前提になっており、Pendingのまま処理が終わるということは想定されていない。

thenのチェーン

then()はいくつでもつなげて書くことが出来る。

function myFunc(num){
    return new Promise((resolve)=>{
        resolve();
    });
};

myFunc()
    .then(()=>{ console.log('1回目のthen') })
    .then(()=>{ console.log('2回目のthen') })
    .then(()=>{ console.log('3回目のthen') });

なぜこのようなことが出来るのかというと、then()がpromiseオブジェクトを返すためである。

function myFunc(){
    return new Promise((resolve)=>{
        resolve();
    });
};

const result = myFunc().then(()=>{ console.log('1回目のthen') });

console.log(result);    // Promise { <pending> }
console.log(result instanceof Promise);   // true
setTimeout(()=>{
    console.log(result);    // Promise { undefined }
}, 0);

myFunc().then()の返り値であるresultがpromiseオブジェクトであることが分かる。

resultは発生直後はPendingとなっており、onFulfilledが実行された後は、undefinedとなっている。
なぜundefinedかというと、onFulfilledの返り値がundefinedだからである。

つまり、onFulfilledonRejectedを実行し、その返り値を値として持つFulfilledなpromiseオブジェクトを返す、というのがthen()の仕様である。

function myFunc(arg){
    return new Promise((resolve, reject)=>{
        arg ? resolve() : reject() ;
    });
};
function onFulFilled(){
    return 1;
};
function onRejected(){
    return 9;
};

const result = myFunc(true).then(onFulFilled, onRejected);
const result2 = myFunc(false).then(onFulFilled, onRejected);

setTimeout(()=>{
    console.log(result);    // Promise { 1 }
    console.log(result2);   // Promise { 9 }
}, 0);

onFulfilledonRejectedの返り値がpromiseオブジェクトだった場合は、それをそのままthen()の返り値として使う。

function double(number) {
  return new Promise(resolve=>{
    resolve(number * 2);
  });
};
function triple(number) {
  return new Promise(resolve=>{
    resolve(number * 3);
  });
};
function dump(number) {
  console.log(number);
  return number;
};

double(10)
  .then(dump)
  .then(triple)
  .then(dump)
  .then(double)
  .then(dump);
// 20
// 60
// 120

Promiseの静的メソッド

Promiseはいくつかの静的メソッドを持つ。

Promise.resolve()

Promise.resolve()は、promiseオブジェクトを返し、new Promise()のショートカットとなるもの。
Promise.resolve(3)とすれば、3という値を持つFulFilledなpromiseオブジェクトを返す。

Promise.resolve(10).then((result)=>{ console.log(result); }); // 10

function myFunc(arg){
  arg *= 2;
  return Promise.resolve(arg);
};

myFunc(3).then((result)=>{
  console.log(result);  // 6
});

先程の、then()の戻り値がpromiseオブジェクトになる仕組みは、内部的にPromise.resolve()を使うことで実現している。
Promise.resolve()でラップすることで、どのような値でもpromiseオブジェクトになるからだ。

Promise.reject()

Promise.reject()は、先程とは逆にRejectedなpromiseオブジェクトを返す。

function myFunc(arg){
  arg *= 2;
  if(arg >= 10){ return Promise.reject(arg); };
  return Promise.resolve(arg);
};

function onFulfilled(arg){
    console.log('ok!', arg);
};
function onRejected(arg){
    console.log('ng!', arg);
};

// ok! 6
myFunc(3).then(onFulfilled, onRejected);

// ng! 10
myFunc(5).then(onFulfilled, onRejected);

Promise.all()

前述のthen()を使えば複数の非同期処理を順番に実行していくことが出来るが、Promiseには、複数の非同期処理をまとめて扱うための仕組みも用意されている。
それが、Promise.all()Promise.race()である。

まずは、Promise.all()
これは、promiseオブジェクトの配列を渡して使う。そして、全てのpromiseオブジェクトがFulfilledとなったとき、then()の第一引数であるonFulfilledが実行される。そこに渡される引数も、配列になっている。
また、いずれかのpromiseオブジェクトがRejectedとなった場合、then()の第二引数であるonRejectedが実行される。

function double(arg){
  return new Promise((resolve, reject)=>{
    arg *= 2;
    if(arg > 9){ reject(new Error('double リミット突破 ' + arg)) }
    else { resolve(arg) };
  });
};

function triple(arg){
  return new Promise((resolve, reject)=>{
    arg *= 3;
    if(arg > 9){ reject(new Error('triple リミット突破 ' + arg)) }
    else { resolve(arg) };
  });
};

function combineFunc(arg){
  const funcList = [double(arg), triple(arg)];
  Promise.all(funcList).then((receivedList)=>{
    console.log(receivedList[0], receivedList[1]);
  }, (error)=>{
    console.log(error);
  });
};

combineFunc(3); // 6 9
combineFunc(4); // Error: triple リミット突破 12
combineFunc(7); // Error: double リミット突破 14

これを使うことで例えば、複数のAjax通信が終わるのを待ってから所定の処理を行う、といったコードも簡単に書ける。

Promise.race()

Promise.race()は、Promise.all()とほとんど同じ仕組み。
唯一の違いは、all()は全てのpromiseオブジェクトがFulfilledになるのを待ったが、race()ではどれか一つでも状態変化が起きれば、その時点でonFulfilledonRejectedが実行される。

なお、race()で競争に負けたpromiseオブジェクト(下記ではslowPromise()の返り値)も、その処理自体は引き続き行われる。
そのため、slow done!というログが出力されている。

function fastPromise(arg){
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      arg ? resolve('fastPromise') : reject(new Error('fastPromise')) ;
      console.log('fast done!');
    }, 100);
  });
};
function slowPromise(arg){
  return new Promise((resolve, reject)=>{
    setTimeout(()=>{
      arg ? resolve('slowPromise') : reject(new Error('slowPromise')) ;
      console.log('slow done!');
    }, 1000);
  });
};

Promise.race([fastPromise(true), slowPromise(true)])
  .then((result)=>{
    console.log(result);
  }, (error)=>{
    console.log(error);
  });
// fast done!
// fastPromise
// slow done!

Promise.race([fastPromise(true), slowPromise(false)])
  .then((result)=>{
    console.log(result);
  }, (error)=>{
    console.log(error);
  });
// fast done!
// fastPromise
// slow done!

Promise.race([fastPromise(false), slowPromise(true)])
  .then((result)=>{
    console.log(result);
  }, (error)=>{
    console.log(error);
  });
// fast done!
// Error: fastPromise
// slow done!

all()とrace()で行われるラッピング

all()race()の引数の配列にpromiseオブジェクト以外のものが渡された場合、Promise.resolve()によってラップされる。
そのため、promiseオブジェクト以外のものが渡されても問題はない。

function delayPromise(ms){
    return () => {
        return new Promise((resolve)=>{
            setTimeout(()=>{
                resolve();
                console.log(ms+' ミリ秒経過。');
            }, ms);
        });
    };
};

const p1000 = delayPromise(1000);
const p2000 = delayPromise(2000);

Promise.all([p1000(), p2000()]).then(()=>{ console.log('全ての処理が完了。'); });
Promise.all([p1000(), p2000(), null, 'hoge']).then(()=>{ console.log('Promiseオブジェクト以外でも問題ない。'); });

ただ、race()の場合、即座にPromise.resolve()が実行されることで、その時点でレースは終わってしまう。

function delayPromise(ms){
    return () => {
        return new Promise((resolve)=>{
            setTimeout(()=>{
                resolve(ms);
                console.log(ms+' ミリ秒経過。');
            }, ms);
        });
    };
};

const p1000 = delayPromise(1000);
const p2000 = delayPromise(2000);

Promise.race([p1000(), p2000()]).then((result)=>{ console.log(result+'がゴールしてレースが完了。'); });
// 1000 ミリ秒経過。
// 1000がゴールしてレースが完了。
// 2000 ミリ秒経過。

Promise.race([p1000(), 'hoge', p2000()]).then((result)=>{ console.log(result+'がゴールしてレースが完了。'); });
// hogeがゴールしてレースが完了。
// 1000 ミリ秒経過。
// 2000 ミリ秒経過。

エラーハンドリング

例外

promiseオブジェクトを生成する際に例外が発生すると、Rejectedなpromiseオブジェクトが返される。

function onFulfilled(arg){
    console.log('Fulfilled');
    console.log(arg);
};
function onRejected(arg){
    console.log('Rejected');
    console.log(arg);
};

function myFunc(){
    return new Promise((resolve, reject)=>{
        throw new Error();
    });
};

myFunc().then(onFulfilled, onRejected);
// Rejected
// Error

catch()

promiseオブジェクトはcatch(onRejected)メソッドを持つ。
これは、then(undefined, onRejected)シンタックスシュガー。

catch()を使うべきか

catch()シンタックスシュガーなので、これを使わなくてもコードは書ける。
だが、次の理由から、catch()を使うほうが望ましいと思われる。

then(onFulfilled, onRejected)を使うと、onFulfilledでエラーが発生した際に、それを拾えないのだ。

function onFulfilled(arg){
    throw new Error();  // 何らかの理由でエラーが発生する
    console.log('Fulfilled');
    console.log(arg);
};
function onRejected(arg){
    console.log('Rejected');
    console.log(arg);
};


Promise.resolve(1).then(onFulfilled, onRejected);
// 何も表示されない

catch()を使えば、同様の状況でもエラーを拾える。
もちろん、大元のpromiseオブジェクトがRejectedになった際も、きちんと拾える。

function onFulfilled(arg){
    throw new Error();  // 何らかの理由でエラーが発生する
    console.log('Fulfilled');
    console.log(arg);
};
function onRejected(arg){
    console.log('Rejected');
    console.log(arg);
};

Promise.resolve(1).then(onFulfilled).catch(onRejected);
// Rejected
// Error

Promise.reject(1).then(onFulfilled).catch(onRejected);
// Rejected
// 1

Rejectedになった際のチェーン

先程、then()の後にcathc()をつなげた。
このとき、具体的にどのような処理が行われているのか。

復習も兼ねてpromiseオブジェクトがどのように受け渡されていくのかを確認していく。

まず、resolve()reject()が行われると、promiseオブジェクトの状態はFulfilledRejectedになる。

console.log(Promise.resolve(1));   // Promise { 1 }
console.log(Promise.reject(1)); // Promise { <rejected> 1 }

そしてこのオブジェクトがthen()メソッドを呼ぶと、状態に応じてonFulfilledonRejectedを実行する。

function onFulfilled(arg){
    console.log('Fulfilled');
};
function onRejected(arg){
    console.log('Rejected');
};

Promise.resolve(1).then(onFulfilled, onRejected);   // Fulfilled
Promise.reject(1).then(onFulfilled, onRejected);    // Rejected

では、そのthen()は何を返すのか。

function onFulfilled(arg){
    return 'Fulfilled';
};
function onRejected(arg){
    return 'Rejected';
};

let fulilledObj = Promise.resolve(1).then(onFulfilled, onRejected);
let rejectedObj = Promise.reject(1).then(onFulfilled, onRejected);

console.log(fulilledObj, rejectedObj);  // Promise { <pending> } Promise { <pending> }

setTimeout(()=>{
    console.log(fulilledObj, rejectedObj);  // Promise { 'Fulfilled' } Promise { 'Rejected' }
    fulilledObj.then(()=>{ console.log('Fulfilledなオブジェクトです。') }); // Fulfilledなオブジェクトです。
    rejectedObj.then(()=>{ console.log('Fulfilledなオブジェクトです。') }); // Fulfilledなオブジェクトです。
}, 0);

まずはPendeing状態のpromiseオブジェクトが返される。
その後、実行されたonFulfilledonRejectedに応じて、値を返す。
そしてその値がpromiseオブジェクトでなかった場合、それをPromise.resolve()でラップしたものを返す。
そのため上記の例では、fulfilledObjrejectedObjも、Fulfilledなpromiseオブジェクトとなる。

では、対応するonFulfilledonRejectedがなかった場合は、どうなるのか。

どちらの場合も、then()に渡されたpromiseオブジェクトをそのまま返す。

function onFulfilled(arg){
    return 'Fulfilled';
};
function onRejected(arg){
    return 'Rejected';
};

let fulilledObj = Promise.resolve(1).then(undefined, onRejected);

console.log(fulilledObj);   // Promise { <pending> }

setTimeout(()=>{
    console.log(fulilledObj);   // Promise { 1 }
    fulilledObj.then(()=>{ console.log('Fulfilledなオブジェクトです。') }); // Fulfilledなオブジェクトです。
}, 0);
function onFulfilled(arg){
    return 'Fulfilled';
};
function onRejected(arg){
    return 'Rejected';
};

let rejectedObj = Promise.reject(1).then(onFulfilled);

console.log(rejectedObj);   // Promise { <pending> }

setTimeout(()=>{
    console.log(rejectedObj);   // Promise { <rejected> 1 }
    rejectedObj.then(()=>{ console.log('Fulfilledなオブジェクトです。') }); // 何も表示されない
}, 0);

そのため、then()catch()がチェーンになっている場合、対応するonFulfilledonRejectedがなかった場合、そこの処理は飛ばして次のthen()catch()に進む、という動作になる。

function onFulfilledA(){
    console.log('A');
};
function onFulfilledB(){
    console.log('B');
};
function onFulfilledC(){
    console.log('C');
};
function onRejected(){
    console.log('reject');
};

Promise.resolve()
    .then(onFulfilledA)
    .then(onFulfilledB)
    .catch(onRejected)
    .then(onFulfilledC);
// A
// B
// C

Promise.reject()
    .then(onFulfilledA)
    .then(onFulfilledB)
    .catch(onRejected)
    .then(onFulfilledC);
// reject
// C

参考資料