はじめに
Promiseって...難しくないですか???
3ヶ月くらい前の私は、Promiseをほとんど理解できてないのにasync/awaitとか使っちゃってたし、様々な記事を読み漁ってもなかなか理解できず、Promiseの正体を掴むのに時間がかかってしまいました。
そんな3ヶ月くらい前の自分にも伝わるように、できる限り丁寧にPromiseを説明していこうと思います。
前提
本記事では、Promise以外のES6の書き方に関しては触れておりません。
アロー関数やテンプレート文字列などを例文で用いているため、わからない方がいましたら下記記事などを参考にしていただけると幸いです。
Promiseとは
JavaScriptは処理を待てない!
まずはじめに、下記コードを実行してみると、どのような結果になるでしょうか。
console.log("1番目");
// 1秒後に実行する処理
setTimeout(() => {
console.log("2番目(1秒後に実行)");
}, 1000);
console.log("3番目");
1番目、2番目、3番目...という順番で実行されることが理想的ではありますね。
しかし、実際には下記のような順序で実行されてしまいます。
1番目
3番目
2番目(1秒後に実行)
JavaScriptは非同期言語であるため、一つ前の実行に時間がかかった場合、実行完了をまたずに次の処理が行われてしまいます。
Promiseは、処理の順序に「お約束」を。
Promiseを日本語に翻訳すると「約束」です。
なので、私はPromiseのことを、処理の順序に「お約束」を取り付けることができるもの、処理を待機することや、その結果に応じて次の処理をすることお約束するものだと思っています。
先ほどの例で詳しく見ていきましょう。
(コードの中身は、後ほど説明していきますので、わからなくて大丈夫です!)
console.log("1番目");
// お約束を取り付けたい処理にPromise
new Promise((resolve) => {
//1秒後に実行する処理
setTimeout(() => {
console.log("2番目(1秒後に実行)");
//無事処理が終わったことを伝える
resolve();
}, 1000);
}).then(() => {
// 処理が無事終わったことを受けとって実行される処理
console.log("3番目");
});
すると、実行結果は下記のようになります。
1番目
2番目(1秒後に実行)
3番目
Promiseを用いることで、理想の処理を実行することができました。
それでは、より詳しくPromiseをみていきましょう。
Promiseには3つの状態がある
Promiseには、PromiseStatusというstatusがあり、3つのstatusがあります。
-
pending: 未解決 (処理が終わるのを待っている状態) -
resolved: 解決済み (処理が終わり、無事成功した状態) -
rejected: 拒否 (処理が失敗に終わってしまった状態)
new Promise()で作られたPromiseオブジェクトは、pendeingというPromiseStatusで作られます。
処理が成功した時に、PromiseStatusはresolvedに変わり,thenに書かれた処理が実行されます。
処理が失敗した時は、PromiseStatusがrejectedに変わり、catchに書かれた処理が実行されます。
Promiseの大まかな処理の流れはわかりましたでしょうか。
それでは実際に書いてみましょう...!
Promiseの書き方
Promiseインスタンスの作成
const promise = new Promise((resolve, reject) => {});
Promiseの引数には関数を渡し、その関数の第一引数にresolveを設定し、第二引数にrejectを任意で設定します。
(resolveもrejectも関数です。)
ちなみに、上記コードにて定義したpromiseをconsole.logすると下記のようなPromiseオブジェクトが返ってきます。
作りたてのPromiseオブジェクトなので、PromiseStatusは、pendeingですね。
resolveさせよう
sample1のコードをresolveさせてみましょう。
// rejectは今回使わないため、引数から削除
const promise = new Promise((resolve) => {
resolve();
}).then(() => {
console.log("resolveしたよ");
});
実行結果は下記のようになります。
resolveしたよ
resolve()を呼び出すことによって、
PromiseStatusはresolvedに変わり,thenの処理が走ってることがわかりますね。
resolve関数に引数を渡してあげると...
resolve関数は、引数を受け取ることができ、次に呼ばれるメソッド(then)の第一引数に渡してあげることができます。
const promise = new Promise((resolve) => {
// 引数に文字列を渡す
resolve("resolveしたよ");
}).then((val) => {
// 第一引数にて、resolve関数で渡した文字列を受け取ることができる
console.log(val);
});
resolveしたよ
resolve関数の引数にて渡された値が、thenの第一引数に渡されてることがわかりますね。
下記コード時点でのpromiseを確認してみましょう。
const promise = new Promise((resolve) => {
// 引数に文字列を渡す
resolve("resolveしたよ");
});
PromiseValueに、resolveに渡した文字列が入ってることがわかりましたね。
PromiseValueに格納された値を、次に呼ばれるメソッドの第一引数に渡すことができる、という仕組みになっております。
rejectさせよう
sample1のコードをrejectさせてみましょう。
const promise = new Promise((resolve, reject) => {
reject();
})
.then(() => {
console.log("resolveしたよ");
})
.catch(() => {
console.log("rejectしたよ");
});
実行結果は下記のようになります。
rejectしたよ
thenの処理は実行されず、catchの処理が実行されることがわかりますね。
Promiseオブジェクトを確認してみると...
想定通り、PromiseStatusがrejectedに.....なってない!!!!!
なぜrejectedにならない...?
下記コード、つまりcatchの処理を行なっていない状態のPromiseオブジェクトをみてみましょう。
const promise = new Promise((resolve, reject) => {
reject();
});
この時点では、PromiseStatusがrejectedですね。
ちょっと安心ですね。笑
(Uncaught (in promise)というエラーはrejectされた時に、一回もcatchの処理が行われなかった時にでます。)
実は、catchにて実行される関数がreturnした値をresolveします。
めちゃくちゃ簡単にいうと、catchはエラー返したら満足して、解決済みだ!ってするみたいです。
つまり、catchにて返されたpromiseオブジェクトのPromiseStatusはresolveになります。
んー、難しい。
Promiseのメソッドチェーン
処理が成功した時にthenに書かれた処理が実行され、処理が失敗した時はcatchに書かれた処理が実行されると説明しましたが、それらの処理の後にまた別のthenを実行することができます。
さらに、returnで返した値を第一引数として次のthenに渡すことが可能です。
const promise = new Promise((resolve, reject) => {
resolve("test");
})
.then((val) => {
console.log(`then1: ${val}`);
return val;
})
.catch((val) => {
console.log(`catch: ${val}`);
return val;
})
.then((val) => {
console.log(`then2: ${val}`);
});
then1: test
then2: test
resolveされたため、1つめのthenが実行されたあと、
returnでvalを受け取り、2つめのthenが実行されました。
const promise = new Promise((resolve, reject) => {
reject("test");
})
.then((val) => {
console.log(`then1: ${val}`);
return val;
})
.catch((val) => {
console.log(`catch: ${val}`);
return val;
})
.then((val) => {
console.log(`then2: ${val}`);
});
catch: test
then2: test
rejectされたため、1つめのthenは処理されず、
1つめのcatchが実行されたあとに2つめのthenが実行されました。
Promise.allとPromise.race
これらは、Promiseを複数実行して、その結果に応じて次の処理に進む便利メソッドです。
Promise.all
Promise.all()は配列でPromiseオブジェクトを渡し、
全てのPromiseオブジェクトがresolvedになったら次の処理に進みます。
const promise1 = new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
}).then(() => {
console.log("promise1おわったよ!");
});
const promise2 = new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 3000);
}).then(() => {
console.log("promise2おわったよ!");
});
Promise.all([promise1, promise2]).then(() => {
console.log("全部おわったよ!");
});
promise1おわったよ!
promise2おわったよ!
全部おわったよ!
Promise.race
Promise.race()はPromise.all()と同じく配列でPromiseオブジェクトを渡し、
どれか1つのPromiseオブジェクトがresolvedになったら次に進みます。
const promise1 = new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
}).then(() => {
console.log("promise1おわったよ!");
});
const promise2 = new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 3000);
}).then(() => {
console.log("promise2おわったよ!");
});
Promise.race([promise1, promise2]).then(() => {
console.log("どれか一つおわったよ!");
});
promise1おわったよ!
どれか一つおわったよ!
promise2おわったよ!
async/awaitを使ってもっと簡潔に!
Promiseの処理は、Promiseインスタンスを生成したり,resolve/rejectしたりするため、場合によっては結構冗長的になってしまいます。
async/awaitを使うとPromiseの処理をより簡潔に書くことができます。
例に、下記のPromiseの処理をasync/awaitを使って、リファクタします。
const alwaysLateBoy = (ms) => {
new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
}).then(() => {
console.log(`すまん!${ms}ms待たせたな。`);
});
};
処理としては、引数で渡したms秒分待機して、その後console.logを走らせれば良さそうですね。
上記の処理をasync/awaitを用いると、下記のようにまとめることができます。
// async function を定義
const alwaysLateBoy = async (ms) => {
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
})
// Promiseの結果が返ってくるまで実行されない
console.log(`すまん!${ms}ms待たせたな。`)
};
asyncとは
asyncは非同期関数を定義する関数宣言であり、関数の頭につけることで、
Promiseオブジェクトを返す関数にすることができます。
そのような関数をasync functionといいます。
const asyncFunc = async () => {
return 1;
};
上記async functionを実行すると、下記のようなPromiseオブジェクトが返ってきます。
async functionが値をreturnした場合、Promiseは戻り値をresolveし、
その値はPromiseValueとして扱われます。
awaitとは
awaitは、Promiseオブジェクトが値を返すのを待つ演算子です。
awaitは必ず、async function内で使います。
下記のコードを例に考えてみましょう。
const asyncFunc = async () => {
let x, y;
x = new Promise(resolve => {
setTimeout(() => {
resolve(1);
}, 1000 )
})
y = new Promise(resolve => {
setTimeout(() => {
resolve(1);
}, 1000 )
})
console.log(x + y)
}
この処理では、Promiseがresolveするのを待つ処理を書いてないため、
console.log(x + y)では、下記のような結果が返ってきます。
NaN
この処理を、Promiseがresolveするのを待つようにするには、
このPromiseオブジェクトが全て実行完了するのを待つPromise.allを使えばできそうですが、awaitを使うともっと簡単に実装できます。
const asyncFunc = async () => {
let x, y;
// Promiseがresolveするまで待機
x = await new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
// Promiseがresolveするまで待機
y = await new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
console.log(x + y);
};
console.log(x + y)は、上二つのPromiseが値を返すのを待ってから実行されるため、実行結果は下記のようになります。
2
おわりに
想像以上に長くなってしまい、後半からわかりづらいかもしれません。。。泣
少しでもPromiseに対して苦手意識がなくなったら良いなと思います。
(自分自身、この記事を書いていくうちに、さらにPromiseを理解できたような気がします。)
