5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

LeetCodeで非同期処理をマスターするぞ!(第2弾)

Last updated at Posted at 2024-01-12

概要

この記事は、LeetCodeの30 Days of JavaScriptのPromises and Time(全8問)を1問ずつ解いて非同期処理をマスターするシリーズの第2弾です。

前回はこちら

問題:2621. Sleep

2621. Sleep

millis (1 <= millis <= 1000) が与えられるので、
millis ミリ秒スリープする非同期関数 sleep を実装する

使用例

let t = Date.now()
sleep(100).then(() => console.log(Date.now() - t)) // 100

.then するので Promise を返す必要がある

必要な知識

非同期処理

私が説明するより、最高に素晴らしいこちらを読んでください

ざっくり

  1. JavaScript はシングルスレッド言語
  2. なので同時に複数の処理はできないし、ある処理を行っている間は次の処理ができない(ブロッキングが起こる)
  3. JavaScript で非同期処理を行いたい場合は、環境(ブラウザとか)が提供してくれている機能である非同期APIを使う

setTimeout

  • 指定した時間より後に指定した関数を実行する非同期API の1つ
  • 呼び出された際にタイマーをスタートさせ、すぐにreturn する(ブロッキングが起こらないようにする)
  • タイマーが完了したら、指定した関数を実行する

(もうちょっとちゃんと書きたかったが、非同期奥が深すぎて断念。次回に期待。)

使い方

setTimeout(functionRef, delay, param1, param2, ...)

引数

  • functionRef:タイマーが満了時に実行するコールバック関数
  • delay:タイマーの時間(ミリ秒、デフォルトは0)
  • paramfunctionRef の引数

返り値

タイマー識別子 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 つのコールバック関数(慣習的に resolvereject )をとる
    • 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 オブジェクトを返すメソッド・関数の前につけると、その promiseresolvereject を返すまで待機する
  • 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

asyncPromise にラップしてくれるのであれば、

async function sleep(millis: number): Promise<void> {
    return setTimeout(()=>{}, millis)
}

でもよいのでは?と思ったが、実行すると、出力が 0 になる
(つまり指定した millis ミリ秒のスリープが起こっていない)


以下、自分なりに考えた理由(間違ってたら教えてください)

setTimeout が呼び出されると

  1. メインスレッド以外のところでタイマー開始
  2. タイマー識別子をすぐに return し、後続の処理を行えるようにする
  3. タイマーが終了したら、タスクキュー(?)にコールバック関数をエンキュー
  4. イベントループがコールバック関数をメインスレッドに持ってきて実行可能

ということが起こるらしいので、単に

return setTimeout(()=>{}, millis)

だと待機が起こらなさそう
(↑ の 「2. タイマー識別子をすぐに return し、後続の処理を行えるようにする」より)

setTimeout 自体を Promise に入れて、タイマーが終了するまで return させないようにする必要がある

つまったところ2

function sleep(millis: number): Promise<void> {
    return new Promise((resolve) => setTimeout(()=>{}, millis));
}

でも行けるのでは??と思ったが、これだと何も出力されない
調べたら、まさにここで説明されていた

  • Promise コンストラクタは引数にコールバック関数 executor を取るが、この executorreturn は無視される
  • 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));
}

にすることで、

  1. setTimeout のタイマー完了後に resolve() が実行される
  2. Promise オブジェクトの状態が Fulfilled になって return される
  3. .then() に渡されて実行される

となるらしい
なるほどなるほど

参考

全部ちゃんと読んで理解したわけではありませんが、
今後の備忘録とちゃんと理解できますようにという願望も込めて一度でも目にしたサイトはすべて載せておきます

非同期処理

Promise まわり(?)

SetTimeout

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?