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()
には引数として関数を渡すが、この第一引数と第二引数はそれぞれ、onFulfilled
、onRejected
と呼ぶのが一般的となっているようだ。
つまり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
のままである。
ただ、x
が1
になっていることから分かるように、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
だからである。
つまり、onFulfilled
やonRejected
を実行し、その返り値を値として持つ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);
onFulfilled
やonRejected
の返り値が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()
ではどれか一つでも状態変化が起きれば、その時点でonFulfilled
かonRejected
が実行される。
なお、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オブジェクトの状態はFulfilled
やRejected
になる。
console.log(Promise.resolve(1)); // Promise { 1 } console.log(Promise.reject(1)); // Promise { <rejected> 1 }
そしてこのオブジェクトがthen()
メソッドを呼ぶと、状態に応じてonFulfilled
かonRejected
を実行する。
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オブジェクトが返される。
その後、実行されたonFulfilled
やonRejected
に応じて、値を返す。
そしてその値がpromiseオブジェクトでなかった場合、それをPromise.resolve()
でラップしたものを返す。
そのため上記の例では、fulfilledObj
もrejectedObj
も、Fulfilled
なpromiseオブジェクトとなる。
では、対応するonFulfilled
やonRejected
がなかった場合は、どうなるのか。
どちらの場合も、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()
がチェーンになっている場合、対応するonFulfilled
やonRejected
がなかった場合、そこの処理は飛ばして次の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