为什么使用Promise.then设置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>

我期望发生的事情:

  • 点击发生
  • 盒子开始水平平移100像素(此操作需要两秒钟)
  • 单击时,Promise还将创建一个新的。里面说PromisesetTimeout功能设置为2秒
  • 操作完成后(经过两秒钟),setTimeout运行其回调函数并将其设置transition为none。完成此操作后,setTimeout还将还原transform为原始值,从而使该框显示在原始位置。
  • 该框出现在原始位置,此处没有过渡效果问题
  • 完成所有这些操作后,将transition框的值设置回其原始值

但是,可以看出,该transitionnone在运行时似乎不是。我知道还有其他方法可以实现上述目的,例如使用关键帧和transitionend,但是为什么会发生这种情况?我明确设置transition回其原始值仅之后setTimeout完成它的回调,从而解决的前景。

编辑

根据要求,下面是显示有问题的行为的代码的gif: 问题


您在哪个浏览器中看到此内容?在Chrome上,我看到了它的意图。
特里

@Terry Windows版Firefox 73.0(64位)。
理查德

您能否在说明问题的问题上附加gif?据我所知,它还在Firefox上按预期方式呈现/表现。
特里

Promise解析后,将还原原始过渡,但此时此框仍会变形。因此,它向后过渡。您需要至少再等待一帧,才能将转换值重置为原始值:jsfiddle.net/khrismuc/3mjwtack
Chris G

多次运行时,我设法重现了一次该问题。过渡执行取决于计算机在后台执行的操作。在解决承诺之前,为延迟增加更多时间可能会有所帮助。
Teemu

Answers:


4

事件循环批量更改样式。如果您在一行上更改元素的样式,浏览器将不会立即显示该更改。它将等到下一个动画帧。例如,这就是为什么

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

不会导致闪烁;浏览器仅关心所有Javascript完成后设置的样式值。

渲染所有Javascript(包括微任务)完成发生。将.then在microtask诺言的发生(如所有其他JavaScript已经完成,这将有效地立即运行,但之前别的-比如渲染-有机会来运行)。

您正在做的是在浏览器开始呈现引起的更改之前,在微任务中将transition属性设置为。''style.transform = ''

如果您在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
CertainPerformance

2
不完全是这样。事件循环并没有说明样式更改。大多数浏览器会尽可能地等待下一个绘画帧,仅此而已。更多信息 ;-)
Kaiido

3

如果元素开始隐藏问题,您将面临过渡的变体不起作用,而是直接在transition属性上。

您可以参考此答案以了解CSSOM和DOM如何链接到“重绘”过程。
基本上,浏览器通常会等到下一个绘画框重新计算所有新框的位置,然后将CSS规则应用于CSSOM。

因此,在你的诺言处理程序中,当您重置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(),您需要了解它们将在呈现步骤之前,在当前任务完成后立即运行,这是事件循环处理模型的第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

有两个好处:(1)然后可以在CSS中修改过渡的持续时间,而无需修改javascript;(2)transitionToPromise.js是可重用的。顺便说一句,我尝试了它,并且效果很好。
Roamer-1888

@ Roamer-1888是的,您可以,尽管我个人会添加一张支票(evt)=>if(evt.propertyName === "transform"){ ...以避免误报,而且我真的不希望这样的事件发生,因为您永远不知道它是否会触发(例如 someAncestor.hide() ,在过渡运行时,您承诺绝不会火,你的过渡将卡住禁用所以这真的取决于OP来确定什么是最适合自己的,但个人和经验,我现在更喜欢比超时事件transitionend。
Kaiido

1
优点和缺点,我猜。无论如何,不​​管有没有promisification,这个答案都比涉及两个setTimeouts和requestAnimationFrame的答案要干净得多。
Roamer-1888

我有一个问题。您说这requestAnimationFrame()将在下一个浏览器重新绘制之前触发。您还提到过,浏览器通常会等到下一个绘画框重新计算所有新框的位置。但是,您仍然需要手动触发强制重排(您在第一个链接上的答案)。因此,我得出的结论是,即使requestAnimationFrame()在重涂之前,浏览器仍然没有计算出最新的计算样式。因此,需要手动强制重新计算样式。正确?
理查德

0

我很欣赏这不是您想要的东西,但是出于好奇并出于完整性考虑,我想看看是否可以编写仅CSS的方法来实现此效果。

几乎 ...但是事实证明,我仍然必须包含一行JavaScript。

工作示例:

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

我相信您的问题只是在您.thentransitionto 设置为时'',应该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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.