Buzzword111
@Buzzword111

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

async/await の await後にevent.preventDefault()されないのはなぜですか?

質問内容

チェックボックス
<label><input type="checkbox" id="c1" value="on">チェックボックス1</label>
$('#c1').get(0).checked = false;
$('#c1').on('click', async function(e) {
    let clickElement = this;
    let checked = clickElement.checked;
    console.log(`checked: ${checked}`);
  //=> true

  let promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      let message = '3秒後です';
      console.log(message);
      resolve(message);
    }, 3000);
  });

  e.preventDefault(); // ← awaitの前で行うとe.preventDefault()される。
  await promise1
  .then(() => {
    console.log('非同期処理成功');
    //clickElement.checked = checked;
  })
  .catch(() => {
    console.log('非同期処理失敗');
    //clickElement.checked = !checked;
  });
  // e.preventDefault(); // ← awaitの後で行うとe.preventDefault()されない。
  console.log('click end');
});

JSFiddle - Code Playground

チェックボックスのclickイベントハンドラで非同期処理を実行させているのですが、e.preventDefault()await promise1の前で実行すると preventDefault()された上で非同期処理をawaitしてくれます。

今回質問したい内容は、タイトルにも書いているのですが e.preventDefault()await promise1の後で実行すると preventDefault()の処理が正常に動かないのです。

動かないという事実はコードを書いて理解したのですが、どういった理由でそのような挙動になるのかご存知の方はいないでしょうか?

自分が把握していること

event.preventDefault()

  • ブラウザのデフォルト動作をキャンセル(防ぐ)処理のこと。
  • event.preventDefault()が実際に動作するのは、イベントハンドラの処理が終了した後に動く。
  • チェックボックスのブラウザデフォルト動作はチェック状態を切り替えるということ。 checkbox の preventDefault

stackoverflowでの参考になりそうな回答

javascript - event.preventDefault in async functions - Stack Overflow
Yes, it is totally possible to call preventDefault() in an async event handler function. You only have to ensure to make the call before the first await, as otherwise the event already will have happened when the function resumes. The event flow will continue and not wait for the promise that the event handler returns.

この回答によれば、awaitで待機する前にpreventDefault()を処理しておかないと、ブラウザのデフォルト動作がすでに動いてしまうらしいです。
非同期処理はawaitで待機されるけど、イベントハンドラー(click)の処理は待機されず awaitと書いた時点で終了扱いとなる?

0

3Answer

ブラウザでは UI の処理と JS の処理が直列的に交互に実行されています。 await は JS の処理を(関数の途中であっても)中断して UI の処理に戻し、 UI が暇になったら Promise を実行して結果を待つ仕組みです。

    イベント発生 v            v イベント処理再開
|------UI-----|=====JS=====|-----------|==|----|======|------->
                 await 開始 ^           ^       ^ await 完了し続きを実行
                                        ` Promise の中身を実行

preventDefault() の前に await するとイベントのデフォルトの処理が進んでしまうので意味がありません。

2Like

Comments

  1. @Buzzword111

    Questioner

    回答感謝します。
    UIの処理とJSの処理が別々であるという認識が自分にはなかったので勉強になります。
    await処理後にはUIの処理に戻るので、結果 preventDefault()されることなくデフォルトの処理が動くということですね。

基本的にUI関係のイベントは、イベントハンドラの処理を終えてからでないと他のイベントを処理できません。
そのためイベントハンドラ内で時間のかかる処理を行うとユーザーから見れば一時的なフリーズと同じ状態になります。
それを防ぐために、時間のかかる処理はawaitによって非同期処理とすることでイベントハンドラの方はawaitに処理を任せた時点で終了させます。
だからawaitより後ろに書いたコードはイベントハンドラ内で実行されるものではなく、await処理の完了後に実行されるものとなります。

2Like

Comments

  1. @Buzzword111

    Questioner

    回答ありがとうございます。
    非同期処理などの時間のかかる処理でUIを一時的にロックする。
    それを防ぐために、イベントハンドラはawaitに処理を任せた時点で終了するということなんですね。

    > awaitより後ろに書いたコードはイベントハンドラ内で実行されるものではなく、await処理の完了後に実行されるものとなります。
    なるほどです。await以降で書いた処理がどのような扱いになるのかも疑問だったので納得です。


    UI関係イベントはイベントハンドラ後に動く。
    なので非同期処理などの重い処理の後に動かすユーザーはいちじ
  2. > 非同期処理などの時間のかかる処理でUIを一時的にロックする

    非同期処理とは「処理の完了を待たず次の処理を進める(後でときどき完了チェックする)」というものなので、UIなどがロックしないようにするためのものです。
    その性格上時間のかかる処理に使うほど効果的ではありますが、非同期処理そのものは時間がかかるかどうかとは無関係です。
  3. @Buzzword111

    Questioner

    補足ありがとうございます。

    > 非同期処理とは「処理の完了を待たず次の処理を進める(後でときどき完了チェックする)」というものなので、UIなどがロックしないようにするためのものです。

    元々の非同期処理の目的を忘れていました。
    UIなどがロックしないようにするために非同期処理で動かすことを考えると、awaitでUIの処理を待つのは本来の目的から外れそうですね。
    理解が深まりました。ありがとうございます。

await後に置いた処理は、Promiseの文脈に入る。
雑に書くと

await promise1
.then(...)
.catch(...)
.finally(() => e.preventDefault())

のような感じ。

Promiseは直ちに実行されるわけではない(約束を結んだだけ)ので、バブリング抑制されないままclickイベントを抜けた形となる。

await前であれば、まだPromiseの文脈に入っていないため、バブリング抑制が機能する。

多分そんな話だと思われる。

1Like

Comments

  1. @Buzzword111

    Questioner

    回答ありがとうございます。

    await以降の処理は、Promise内の処理扱いとなるためclickイベントとは別物になるということですね。

    promise1.finally(() => preventDefault())
    というのはなるほど! と思いました!

Your answer might help someone💌