概要
この記事は、LeetCodeの30 Days of JavaScriptのPromises and Time(全8問)を1問ずつ解いて非同期処理をマスターするシリーズの第2弾です。
前回はこちら
問題:2621. Sleep
millis
(1 <= millis
<= 1000) が与えられるので、
millis
ミリ秒スリープする非同期関数 sleep
を実装する
使用例
let t = Date.now()
sleep(100).then(() => console.log(Date.now() - t)) // 100
.then
するので Promise
を返す必要がある
必要な知識
非同期処理
私が説明するより、最高に素晴らしいこちらを読んでください
ざっくり
- JavaScript はシングルスレッド言語
- なので同時に複数の処理はできないし、ある処理を行っている間は次の処理ができない(ブロッキングが起こる)
- JavaScript で非同期処理を行いたい場合は、環境(ブラウザとか)が提供してくれている機能である非同期APIを使う
setTimeout
- 指定した時間より後に指定した関数を実行する非同期API の1つ
- 呼び出された際にタイマーをスタートさせ、すぐにreturn する(ブロッキングが起こらないようにする)
- タイマーが完了したら、指定した関数を実行する
(もうちょっとちゃんと書きたかったが、非同期奥が深すぎて断念。次回に期待。)
使い方
setTimeout(functionRef, delay, param1, param2, ...)
引数
-
functionRef
:タイマーが満了時に実行するコールバック関数 -
delay
:タイマーの時間(ミリ秒、デフォルトは0) -
param
:functionRef
の引数
返り値
タイマー識別子 timerId
(中身は正の整数値)
Promise
オブジェクト
-
非同期処理のコードを人間が簡単に使えるようにできるオブジェクト(インスタンス?)
- Promise がない時代は コールバック地獄 (ネストが深くて複雑なコードになっていた)らしい
- ちゃんとした有用性の説明はこちら
-
3つの State と 2つの Fate をもつ
State
Promise オブジェクトは下の 3 つのいずれかの State をとる(同時に複数の State はとれない)
- Pending(待機状態):初期状態。成功も失敗もしていない
- Fulfilled(履行状態):処理が成功したことを意味する
- Rejected(拒否状態):処理が失敗したことを意味する
Promise()
コンストラクタ
- 主にまだ
Promise
に対応していない関数をラップするために使用 -
new
演算子と併用して使用することでPromise
オブジェクト(Promise
インスタンス)を生成できる
使い方
new Promise(executor)
引数
executor
- 新しい
Promise
オブジェクトを構築する過程でコンストラクターによって呼び出されるコールバック関数 - return による返り値は無視される
- 引数として 2 つのコールバック関数(慣習的に
resolve
・reject
)をとる-
resolve
:
exector
を呼び出しているPromise
オブジェクトの State を Fulfilled にしたい場合に呼び出す -
reject
(省略可):
exector
を呼び出しているPromise
オブジェクトの State を Rejected にしたい場合に呼び出す
-
コールバック関数:
引数として他の関数に渡され、外側の関数の中で呼び出されて、何らかのルーチンやアクションを完了させる関数
アロー関数を使って書くのをよく見る
const promise = new Promise((resolve, reject) => {
if (Math.random() < 0.5) {
resolve("Promise履行時の値");
} else {
reject("Promise拒否時の理由");
}
});
返り値
Promise
オブジェクト
おまけ
Promise.resolve()
( Promise
の静的メソッド)でも同じようなことができるらしい
const promise1 = Promise.resolve("Promise履行時の値");
// この2つは大体同じ
const promise2 = new Promise(resolve => resolve("Promise履行時の値"));
大体一緒だが厳密には違うらしいので注意
async
- 関数やメソッドの前につけると、その関数・メソッドは Promise オブジェクトを返す
async function requestAsync(): Promise<number> {
return 1;
}
// requestAsyncはこれと同じ
function request(): Promise<number> {
return new Promise((resolve) => {
resolve(1);
});
}
await
- Promise オブジェクトを返すメソッド・関数の前につけると、その
promise
がresolve
かreject
を返すまで待機する -
async
をつけたメソッド・関数内でしか使えない
実装例
その1- 原始的な方法
最悪
async function sleep(millis: number): Promise<void> {
let t = Date.now()
while( (Date.now()) - t <= millis ) {}
return
}
でも通る
(Promise
を返す必要があるので、async
関数にしている)
その2 - 文明の利器を使用する方法
setTimeout
を使った方法
function sleep(millis: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, millis));
}
今回は問題文で
It can resolve any value.
と言っているので、return
の代わりに await
でもOK
つまったところ1
async
は Promise
にラップしてくれるのであれば、
async function sleep(millis: number): Promise<void> {
return setTimeout(()=>{}, millis)
}
でもよいのでは?と思ったが、実行すると、出力が 0 になる
(つまり指定した millis
ミリ秒のスリープが起こっていない)
以下、自分なりに考えた理由(間違ってたら教えてください)
setTimeout
が呼び出されると
- メインスレッド以外のところでタイマー開始
- タイマー識別子をすぐに return し、後続の処理を行えるようにする
- タイマーが終了したら、タスクキュー(?)にコールバック関数をエンキュー
- イベントループがコールバック関数をメインスレッドに持ってきて実行可能
ということが起こるらしいので、単に
return setTimeout(()=>{}, millis)
だと待機が起こらなさそう
(↑ の 「2. タイマー識別子をすぐに return し、後続の処理を行えるようにする」より)
setTimeout
自体を Promise に入れて、タイマーが終了するまで return させないようにする必要がある
つまったところ2
function sleep(millis: number): Promise<void> {
return new Promise((resolve) => setTimeout(()=>{}, millis));
}
でも行けるのでは??と思ったが、これだと何も出力されない
調べたら、まさにここで説明されていた
- Promise コンストラクタは引数にコールバック関数
executor
を取るが、このexecutor
のreturn
は無視される -
executor
の引数であるresolve()
(とreject()
) に呼び出されたもののみが、Promise オブジェクトに影響を与える -
.then()
は Promise オブジェクトの状態が Fulfilled の時に呼び出される
つまり、
上のコードだと引数の resolve()
を呼び出していないので、
sleep
関数から返されるPromise オブジェクトの状態は永遠に Pending のまま
なので .then()
が実行されない
function sleep(millis: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, millis));
}
にすることで、
-
setTimeout
のタイマー完了後にresolve()
が実行される - Promise オブジェクトの状態が Fulfilled になって return される
-
.then()
に渡されて実行される
となるらしい
なるほどなるほど
参考
全部ちゃんと読んで理解したわけではありませんが、
今後の備忘録とちゃんと理解できますようにという願望も込めて一度でも目にしたサイトはすべて載せておきます