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!');
};
});
};
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);
console.log(typeof promiseObj1.then)
const promiseObj2 = myFunc('hoge');
console.log('then' in promiseObj2);
console.log(typeof promiseObj2.then)
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)=>{
(num < 10) ? resolve(num) : reject(num) ;
});
};
myFunc(3).then(onFulfilled, onRejected);
myFunc(10).then(onFulfilled, onRejected);
なお、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);
console.log(obj);
}, 0);
setTimeout(()=>{
console.log(x);
console.log(obj);
obj.then(onFulfilled);
}, 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);
console.log(result instanceof Promise);
setTimeout(()=>{
console.log(result);
}, 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);
console.log(result2);
}, 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);
Promiseの静的メソッド
Promise
はいくつかの静的メソッドを持つ。
Promise.resolve()
Promise.resolve()
は、promiseオブジェクトを返し、new Promise()
のショートカットとなるもの。
Promise.resolve(3)
とすれば、3
という値を持つFulFilled
なpromiseオブジェクトを返す。
Promise.resolve(10).then((result)=>{ console.log(result); });
function myFunc(arg){
arg *= 2;
return Promise.resolve(arg);
};
myFunc(3).then((result)=>{
console.log(result);
});
先程の、then()
の戻り値がpromiseオブジェクトになる仕組みは、内部的にPromise.resolve()
を使うことで実現している。
Promise.resolve()
でラップすることで、どのような値でもpromiseオブジェクトになるからだ。
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);
};
myFunc(3).then(onFulfilled, onRejected);
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);
combineFunc(4);
combineFunc(7);
これを使うことで例えば、複数の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);
});
Promise.race([fastPromise(true), slowPromise(false)])
.then((result)=>{
console.log(result);
}, (error)=>{
console.log(error);
});
Promise.race([fastPromise(false), slowPromise(true)])
.then((result)=>{
console.log(result);
}, (error)=>{
console.log(error);
});
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+'がゴールしてレースが完了。'); });
Promise.race([p1000(), 'hoge', p2000()]).then((result)=>{ console.log(result+'がゴールしてレースが完了。'); });
エラーハンドリング
例外
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);
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);
Promise.reject(1).then(onFulfilled).catch(onRejected);
Rejectedになった際のチェーン
先程、then()
の後にcathc()
をつなげた。
このとき、具体的にどのような処理が行われているのか。
復習も兼ねてpromiseオブジェクトがどのように受け渡されていくのかを確認していく。
まず、resolve()
やreject()
が行われると、promiseオブジェクトの状態はFulfilled
やRejected
になる。
console.log(Promise.resolve(1));
console.log(Promise.reject(1));
そしてこのオブジェクトがthen()
メソッドを呼ぶと、状態に応じてonFulfilled
かonRejected
を実行する。
function onFulfilled(arg){
console.log('Fulfilled');
};
function onRejected(arg){
console.log('Rejected');
};
Promise.resolve(1).then(onFulfilled, onRejected);
Promise.reject(1).then(onFulfilled, onRejected);
では、その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);
setTimeout(()=>{
console.log(fulilledObj, rejectedObj);
fulilledObj.then(()=>{ console.log('Fulfilledなオブジェクトです。') });
rejectedObj.then(()=>{ console.log('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);
setTimeout(()=>{
console.log(fulilledObj);
fulilledObj.then(()=>{ console.log('Fulfilledなオブジェクトです。') });
}, 0);
function onFulfilled(arg){
return 'Fulfilled';
};
function onRejected(arg){
return 'Rejected';
};
let rejectedObj = Promise.reject(1).then(onFulfilled);
console.log(rejectedObj);
setTimeout(()=>{
console.log(rejectedObj);
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);
Promise.reject()
.then(onFulfilledA)
.then(onFulfilledB)
.catch(onRejected)
.then(onFulfilledC);
参考資料