Promiseを使用してCSSプロパティを設定すると、thenブロックで実際に設定されないのはなぜですか?


11

次のスニペットを実行してから、ボックスをクリックしてください。

const box = document.querySelector('.box')
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)'
    new Promise(resolve => {
      setTimeout(() => {
        box.style.transition = 'none'
        box.style.transform = ''
        resolve('Transition complete')
      }, 2000)
    }).then(() => {
      box.style.transition = ''
    })
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class = "box"></div>

私が起こりそうなこと:

  • クリックが発生
  • ボックスは100pxだけ水平方向に平行移動を開始します(このアクションには2秒かかります)
  • クリックすると、新しいPromiseものも作成されます。内部ではPromisesetTimeout関数は2秒に設定されています
  • アクションが完了した(2秒が経過した)後、setTimeoutそのコールバック関数を実行し、transitionnoneに設定します。その後、setTimeouttransform元の値に戻り、ボックスを元の場所に表示するようにレンダリングします。
  • ボックスは元の場所に表示され、ここでは遷移効果の問題はありません
  • これらすべてが完了しtransitionたら、ボックスの値を元の値に戻します

ただし、transitionご覧のようにnone、実行時には値が表示されないようです。上記を実現する他の方法、たとえばキーフレームやを使用する方法があることは知っていますtransitionendが、なぜこれが発生するのですか?コールバックが終了した後でtransitionのみ、明示的に元の値に戻し、Promiseを解決しました。setTimeout

編集

リクエストに従って、問題のある動作を表示するコードのgifを以下に示します。 問題


これはどのブラウザで見ていますか?Chromeでは、それが意図されているものを見ています。
テリー

@Terry Firefox 73.0(64ビット)for Windows。
リチャード

問題を説明するgifを質問に添付できますか?私が知る限り、Firefoxで期待どおりにレンダリング/動作しています。
テリー

Promiseが解決すると、元の遷移が復元されますが、この時点でボックスはまだ変換されています。したがって、元に戻ります。トランジションを元の値にリセットする前に、少なくとも1フレーム待機する必要があります:jsfiddle.net/khrismuc/3mjwtack
Chris G

複数回実行すると、問題を1回再現することができました。トランジションの実行は、コンピューターがバックグラウンドで何をしているかによって異なります。約束を解決する前に、遅延にさらに時間を追加すると役立つ場合があります。
Teemu

回答:


4

イベントループのバッチスタイルの変更。要素のスタイルを1行で変更した場合、ブラウザはその変更をすぐには表示しません。次のアニメーションフレームまで待機します。これが理由です

elm.style.width = '10px';
elm.style.width = '100px';

ちらつきは発生しません。ブラウザは、すべてのJavaScriptが完了した後で設定されたスタイル値のみを考慮します。

レンダリングは、マイクロタスクを含むすべてのJavaScriptが完了した後に行わます。約束のマイクロタスクで発生する(効果的にすぐに他のすべてのJavascriptが終了したとしてとして実行されますが、何か他のものの前に-レンダリングなど-実行する機会がありました)。.then

あなたがしていることは、ブラウザがによって引き起こされた変更のレンダリングを開始する前に、マイクロタスクでtransitionプロパティを''に設定していることですstyle.transform = ''

a requestAnimationFrame(次の再描画の直前に実行されます)の後で空の文字列への遷移をリセットし、次に(次の再描画の直後に実行されます)の後に遷移をリセットすると、setTimeout期待どおりに動作します。

const box = document.querySelector('.box')
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)'
    setTimeout(() => {
      box.style.transition = 'none'
      box.style.transform = ''
      // resolve('Transition complete')
      requestAnimationFrame(() => {
        setTimeout(() => {
          box.style.transition = ''
        });
      });
    }, 2000)
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class="box"></div>


これが私が探していた答えです。ありがとう。おそらく、これらのマイクロタスクを説明し、力学再描画するリンクを提供できますか?
リチャード

これは良い要約のように見えます:javascript.info/event-loop
特定のパフォーマンス

2
それは正確には真実ではありません。イベントループは、スタイルの変更について何も言いません。ほとんどのブラウザーは、可能な場合は次のペインティングフレームを待機しようとしますが、それはそれで終わりです。詳細 ;-)
海洋堂

3

要素が非表示の問題を開始する場合遷移のバリエーションが機能せず、直接transitionプロパティで直面しています。

CSSOMとDOMが「再描画」プロセスでどのようにリンクされているかを理解するには、この回答を参照しください。
基本的に、ブラウザは通常、次のペインティングフレームがすべての新しいボックスの位置を再計算し、CSSルールをCSSOMに適用するまで待機します。

したがって、Promiseハンドラーでをにリセットしtransitionても""transform: ""まだは計算されていません。計算されると、transitionは既ににリセットされて""おり、CSSOMは変換更新の遷移をトリガーします。

ただし、ブラウザーに強制的に「リフロー」をトリガーさせることができるため、遷移をにリセットする前に、エレメントの位置を再計算させることができます""

これにより、Promiseの使用がまったく不要になります。

const box = document.querySelector('.box')
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)'
    setTimeout(() => {
      box.style.transition = 'none'
      box.style.transform = ''
      box.offsetWidth; // this triggers a reflow
      // even synchronously
      box.style.transition = ''
    }, 2000)
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class = "box"></div>


などのマイクロタスク、上の説明のためにPromise.resolve()またはMutationEvents、またはqueueMicrotask()、あなたは現在のタスクが、行われているように、彼らはすぐにRANを得ることができます理解する必要があります。イベントループ処理モデルの第7ステップを前に、レンダリング段階
したがって、あなたの場合、それは同期的に実行された場合と非常に似ています。

ちなみに、マイクロタスクはwhileループと同じくらいブロックになる可能性があることに注意してください:

// this will freeze your page just like a while(1) loop
const makeProm = ()=> Promise.resolve().then( makeProm );

はい、正確ですが、transitionendイベントに応答することで、遷移の終了に合わせてタイムアウトをハードコードする必要がなくなります。transitionToPromise.jsは、ユーザーが記述できるように移行を約束しますtransitionToPromise(box, 'transform', 'translateX(100px)').then(() => /* four lines as per answer above */)
Roamer-1888

2つの利点:(1)JavaScriptを変更する必要なく、CSSでトランジションの期間を変更できます。(2)transitionToPromise.jsは再利用可能です。ところで、私はそれを試しました、そしてそれはうまくいきます。
Roamer-1888

@ Roamer-1888はい、できますが、(evt)=>if(evt.propertyName === "transform"){ ...誤検知を回避するために個人的にチェックを追加しますが、イベントが発生するかどうかわからないため、このようなイベントを約束することはあまり好きではありません(someAncestor.hide() トランジションが実行されるときのような場合を考え てください。あなたの約束は決して火とあなたの移行が動けなくなります無効になっているが、個人的に経験することによって、彼らのために最善のものを決定するためにOPには本当に最高ですので、私は今transitionendイベントよりもタイムアウトを好む。。
Kaiido

1
長所と短所、私は推測します。いずれにしても、約束の有無にかかわらず、この答えは2つのsetTimeoutsとrequestAnimationFrameを含むものよりもはるかに明確です。
Roamer-1888

ただ一つ質問があります。あなたはそれrequestAnimationFrame()が次のブラウザの再描画の直前にトリガーされると述べました。また、ブラウザは通常、次のペインティングフレームがすべての新しいボックスの位置を再計算するまで待機することも述べました。それでも、手動で強制リフローをトリガーする必要がありました(最初のリンクの回答)。したがって、私は、requestAnimationFrame()再描画の直前に起こったとしても、ブラウザーが最新の計算されたスタイルをまだ計算していないという結論を導き出します。したがって、スタイルの再計算を手動で強制する必要があります。正しい?
リチャード

0

私はこれがあなたが探しているものではないことに感謝しますが、好奇心から、そして完全を期すために、この効果に対してCSSのみのアプローチを書くことができるかどうかを見たかったのです。

ほとんど ...しかし、それでも私はJavaScriptの1行を含める必要があったことがわかりました。

実例:

document.querySelector('.box').addEventListener('animationend', (e) => e.target.blur());
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  cursor: pointer;
}

.box:focus {
 animation: boxAnimation 2s ease;
}

@keyframes boxAnimation {
  100% {transform: translateX(100px);}
}
<div class="box" tabindex="0"></div>


-1

あなたの問題は、タイマーコールバックで行ったようにをに設定する必要があるときに.thentransition''に設定していることだけだと思いますnone

const box = document.querySelector('.box');
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)';
    new Promise(resolve => {
      setTimeout(() => {
        box.style.transition = 'none';
        box.style.transform = '';
        resolve('Transition complete');
      }, 2000)
    }).then(() => {
     box.style.transition = 'none'; // <<----
    })
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class = "box"></div>


1
いいえ、OPがためにそれを設定している''あなたのコードは、単純にそれを設定します(要素ルールにより却下された)、再びクラスルールを適用するために'none'戻って移行からボックスを防止するが、元の推移(クラスの)を復元しない、二回
クリスG
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.