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