はじめに
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を理解できたような気がします。)