質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.37%

for文内にsetTimeoutがある関数に対して、promise(async/await)を用いて非同期処理をスマートに記述したい。

解決済

回答 1

投稿

  • 評価
  • クリップ 1
  • VIEW 554

score 360

いつもお世話になっております、今実現したいことはタイトル通りです。
下記のコードを実行すると、当然先に「Done」が出力されてしまいます。
そこでaddTypingMovement内でPromiseを返すようにして、thenで繋げてfinishTitleCallを実行しようと思っているのですが、その場合どのようにresolveを記述すればよいのか分かりません。
一応最終ループ時(iword.length - 1の時)にさらに200msずらしてresolveすれば期待通りの動作にはなるのですが、それではPromiseを使う意味がほぼ無いですし、分からないなりにベストプラクティスでは無いだろうという事を薄々感じています。

自分なりに調べてみたのですが、こういったケースに参考になる記事等が見つけられず、手詰まりになっています。
そもそもPromiseに対する理解があまり深くなく、的外れな質問をしてしまっていたら申し訳ありません。

ですが宜しければ、お力添えをよろしくお願いいたします。

サンプル → https://jsfiddle.net/rdvy39b8/

<p id="title"></p>
<p id="result"></p>
const addTypingMovement = (word, target) => {
  const wordArray = [...word];
  const printTarget = document.querySelector(target);
  for(let i = 0; i < wordArray.length; i++) {
    setTimeout(() => {
      printTarget.textContent = printTarget.textContent + wordArray[i];  
    }, i * 200)
  }
}

const finishTitleCall = () => {
  document.querySelector('#result').textContent = 'Done.'
}

addTypingMovement('Hello World.', '#typing');
finishTitleCall();
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

こんにちは

この回答では3つのコードを挙げます。ご質問のタイトルに、

for文内にsetTimeoutがある関数に対して、・・・

とあったので、 まずご質問のコードにある forループを生かしたコードを回答します。その後、for文を使わないコードを2つ挙げます。

1. forループで、各文字を追加するPromiseの配列を作る。

各文字を追加していく処理をひとつのPromiseにして、それらがすべてresolve されたら、Doneを表示するという前後関係を担保するために、 Promise.all を使えばよいかと思います。

以下、ご質問に挙げられているコードに、上記の趣旨の追加をしたものです。

const addTypingMovement = (word, target) => {
  const promises = [];
  const wordArray = [...word];
  const printTarget = document.querySelector(target);

  for(let i = 0; i < wordArray.length; i++) {    
    const p = new Promise(resolve => {
      setTimeout(() => {
        printTarget.textContent = printTarget.textContent + wordArray[i];
        resolve();
      }, i * 200)
    });    
    promises.push(p);
  }

  return promises;
}

const finishTitleCall = () => {
  document.querySelector('#result').textContent = 'Done.'
}

Promise.all(addTypingMovement('Hello World.', '#typing')).then(finishTitleCall);

以下は、ご質問に挙げられている jsFiddle をFork して、上記の修正版にしたものです。

2. forの替わりにmapを、前後関係の制御にasync/awaitを使用

上記 1. のコードで、Promiseの配列を作るところの for文を map を使って書き換え、 すべての文字が target に追加されたら、Done を表示するために asyncawait を使った例が以下です。

const addTypingMovement = (word, target) => {

  const printTarget = document.querySelector(target);

  return [...word].map((char, i) => new Promise(
    resolve => {
      setTimeout(() => {
        printTarget.textContent += char;
        resolve();
      }, i * 200);
    })    
  );

}

const finishTitleCall = () => {
  document.querySelector('#result').textContent = 'Done.';
}

(async () => {
  await  Promise.all(addTypingMovement('Hello World.', '#typing'));
  finishTitleCall();
})();

3. 最後の文字まで表示するのを一つのPromiseにする。

上記の 1. と 2. とは異なり、「200ミリ秒間隔でひと文字ずつ、最後の文字まで表示する」という処理をひとつの Promise にすることもできます。以下のそのコード例です。

const addTypingMovement = (word, target) => new Promise(
  resolve => {
    const printTarget = document.querySelector(target);
    let i = 0;  
    const intervalId = setInterval(
      () => {
        if (i < word.length) {
          printTarget.textContent = word.substring(0, ++ i);
        } else {
          clearInterval(intervalId);
          resolve('Done.');          
        }        
     }, 200);
  }
);

const finishTitleCall = (message) => {
  document.querySelector('#result').textContent = message;
}

(async () => {
  const message = await addTypingMovement('Hello World.', '#typing');
  finishTitleCall(message);
})();

上記では、 

  • setTimeoutの替わりにsetIntervalを使っています。
  • 文字列全体が表示されるまでをひとつの Promise にするので、 Promise.all  は使わなくなっています。
  • Done.という文字列を resolveの引数で返させるようにしました。

以上、参考になれば幸いです。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/10/13 05:18 編集

    ご回答ありがとうございます、複数の方法も教えていただき恐縮です。
    自分では思いつかない方法ばかりでとても勉強になりました。

    ただ自分の理解力が足りず、2つ目のasync/awaitを用いた例のコードに不明な点があるため少し質問させて下さい。
    まずaddTypingMovementは、word.length個のPromiseオブジェクトを値に持つ配列をreturnする関数であり、
    各Promiseオブジェクトのコールバックに、配列のindex * 200ms後に処理を実行するタイマーが格納されている、という事でしょうか?
    私の感覚では、飽くまでPromiseを返す関数というより、mapにより配列を返す関数を実行しているように見え、どのように動いているのかイマイチ理解できませんでした。
    各Promiseオブジェクトのタイマーはどのタイミングで呼ばれているのでしょうか?

    キャンセル

  • 2019/10/13 13:57

    コメントありがとうございます。

    > 2つ目のasync/awaitを用いた例のコード

    へのご質問に回答します。

    > まずaddTypingMovementは、word.length個のPromiseオブジェクトを値に持つ配列をreturnする
    > 関数であり、各Promiseオブジェクトのコールバックに、配列のindex * 200ms後に処理を
    > 実行するタイマーが格納されている、という事でしょうか?

    はい。そのご理解で合っています。

    回答に書いた3つのコードの中で、関数addTypingMovementが Promiseオブジェクトを返しているのは、3つ目のコードのみです。


    > 各Promiseオブジェクトのタイマーはどのタイミングで呼ばれているのでしょうか?

    setTimeout関数が呼ばれるタイミング(= タイマーがスタートするとき)は、Promiseオブジェクトが new で作成されるときです。それがより明確に分かるコード例を以下に書きました。

    https://jsfiddle.net/jun68ykt/xh6wksyc/4/


    上記の例では、各 Promise に渡すコールバックでは、 resolve も reject も受け取っておらず、単に、各文字で1秒ずつ長い時間でタイマーをセットして、タイムアウトしたらその文字を console に出力しています。Promiseの基礎知識として、Promiseオブジェクトには以下の3つの状態

    ・Pending --- promiseオブジェクトが作成されたときの状態
    ・Fulfilled --- resolve(成功)した時の状態
    ・Rejected --- reject(失敗)した時の状態

    がありますが、上記の例では、resolveもrejectもしていないので、各Promiseオブジェクトの状態はPendingのままですが、各Promiseのコールバック本体にある setTimeout で設定された関数が、指定の時間後に実行されて、consoleに、1秒ごとに h,e,l,l,o と一文字ずつ表示されるのを確認できると思います。

    蛇足ですが、Promise の基礎を押さえるには、以下をお勧め致します。

    JavaScript Promiseの本 ( @azu_re さん著 )
    https://azu.github.io/promises-book/

    まずは上記の
     Chapter.1 - Promiseとは何か
     Chapter.2 - Promiseの書き方
    を読むと、かなり整理できると思います。

    キャンセル

  • 2019/10/14 05:28

    ご返信ありがとうございます。

    頂いたコードを動かしながらなんとか理解できました。
    Promiseに関して適当に理解した気になっていた部分が多々あり、質問の内容とはズレたところでお手数をおかけしてしまい申し訳ありませんでした。
    Promiseの本を参考に一から学びなおしてみます。

    改めまして、複数回にも渡ってご回答頂き本当にありがとうございました。

    キャンセル

  • 2019/10/14 08:24

    どういたしまして。
    > 頂いたコードを動かしながらなんとか理解できました。
    とのことでよかったです 👏

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.37%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る