在样式尚未应用之前触发setTimeout(fn,0)

3
当我想要让执行的javascript暂停,允许浏览器应用样式等操作后再继续时,我倾向于使用常见技术 setTimeout 延迟 0 来在事件循环末尾排队回调。然而,我遇到一种情况,这种方法似乎不能可靠地工作。
在下面的代码片段中,我有一个将过渡效果应用于 chaser 元素的 active 类。
当我悬停在目标 div 上时,我想从 chaser 元素中删除 active 类,将 chaser 移动到新位置,然后重新应用 active 类。效果应该是 o 应立即消失,然后淡入到新位置。但实际上,opacitytop 都应用了过渡效果,因此 o 会从一个位置滑动到另一个位置,而不是立即消失,大部分时间都是这样。
如果我将内部 timeout 的延迟增加到 10,它就开始按照我的初衷行事。如果我将其设置为 5,则有时会正常工作,有时不会。
我本来希望任何 setTimeout 都会在应用样式更新后排队我的回调,但这里明显存在竞争条件。我错过了什么吗?有没有一种方法可以保证更新的顺序?
我使用的是macOS和Windows上的Chrome 56,尚未测试其他浏览器。(我知道我可以通过将过渡效果应用于仅 opacity 属性等其他方式来实现此目的,请将此视为一个人为的例子,以说明关于排序样式更新的特定问题)。

var targets = document.querySelectorAll('.target');
var chaser = document.querySelector('#chaser');
for (var i = 0; i < targets.length; i++) {
  targets[i].addEventListener('mouseenter', function(event) {
    chaser.className = '';
    setTimeout(function() {
      // at this point, I'm expecting no transition
      // to be active on the element
      chaser.style.top = event.target.offsetTop + "px";
      
      setTimeout(function() {
        // at this point, I'm expecting the element to
        // have finished moving to its new position

        chaser.className = 'active';
      }, 0);
    }, 0);
  });
}
#chaser {
  position: absolute;
  opacity: 0;
}
#chaser.active {
  transition: all 1s;
  opacity: 1;
}
.target {
  height: 30px;
  width: 30px;
  margin: 10px;
  background: #ddd;
}
<div id="chaser">o</div>
<div class="target">x</div>
<div class="target">x</div>
<div class="target">x</div>
<div class="target">x</div>
<div class="target">x</div>


1
如果您使用requestAnimationFrame而不是setTimeout(fn,0),会怎样呢?如果您进行双重requestAnimationFrame,那么我认为浏览器应该已经完成了更新。 - David
@David可能有效,不确定是否提供任何保证,但我会做些阅读。然而,更让我担心的是setTimeout没有按照我的期望进行操作,因为我过去一直依赖它,现在我想知道还有什么其他的可能出问题。 - CupawnTae
@David 初步的单个 requestAnimationFrame 实验表明它大多数时候都有效,但并非总是如此。我猜第二个 requestAnimationFrame 可能会实现一致性,但我认为这可能只是因为你基本上延迟了足够长的时间以避免竞争条件 - 我真的希望有些确定性的东西... - CupawnTae
3个回答

1
你需要注意的是在进行任何其他操作之前听取 transitionend 事件。你可以阅读关于 transitionend 事件的 MDN。顺便说一下,setTimeout 不应该被用来保证时间。

编辑:在OP的澄清后,此处为参考。每当元素发生样式更改时,都会发生回流和/或重绘。您可以在这里阅读更多相关信息。如果第二个setTimeout在第一个回流之前运行,则会出现滑动效果。之所以10ms会产生预期的效果,是因为在调整offsetTop属性后(导致元素更改其offsetTop后应用了transition属性),才添加了.active类。通常情况下,有60fps(即每帧约16ms),这意味着在应用新样式之前有16ms的时间窗口可以进行任何操作。这就是为什么小延迟5ms有时会导致不同结果的原因。

简而言之-浏览器每16ms请求JS和CSS进行任何更新,并计算要绘制的内容。如果错过了16ms的时间窗口,可能会得到完全不同的结果。


谢谢,但我不想等待转换完成 - 如果正在进行中,我想在中途中止它,这实际上可以正常工作,但第二部分的行为很奇怪,在这种情况下不应该有转换。 - CupawnTae
但这正是我想要发生的事情。实际上,最初我没有外部的 setTimeout - 我一步完成了删除类和更改 top,然后使用 setTimeout 重新添加 active 类。我添加了额外的步骤只是为了防止它引起问题,但它并没有。 - CupawnTae
你只是想避免“滑入”效果吗?即:如果我悬停在目标上,o就会出现? - Bwaxxlo
我不仅仅想要那个 - 就像我在问题中所说的,我可以通过仅将转换应用于opacity来实现。 我想知道为什么会发生这种情况,特别是setTimeout在这里是否可靠,以及是否有一种方法可以保证应用样式的顺序。 - CupawnTae
我认为你把CSS过渡和JS执行混淆了。CSS过渡从不排队在JS执行堆栈中。setTimeout将在JS堆栈为空时运行。无论是否有CSS过渡正在进行,这都没有关系。 - Bwaxxlo
显示剩余4条评论

0

你正在另一个setTimeout()定时方法内调用setTimeout()定时方法。

我的想法是,为什么不像这样分别调用两个setTimeout()方法:

第一个setTimeout()方法应该先执行,然后在执行结束时,它应该调用第二个setTimeout()方法。


我不明白你所建议的与我的方法有何不同 - 第二个 setTimeout 在第一个 setTimeout 的回调执行结束后才会发生。 - CupawnTae
我想说的是,最好分别调用两个setTimeout()方法。 - John Zenith
如果您按顺序依次调用它们,它将把两个回调都粘贴到事件循环队列的末尾,然后它们将按顺序执行,而不需要在它们之间运行另一个事件循环。我的目标是允许事件循环在第一个和第二个回调之间运行(并更新样式,但不幸的是这没有发生),这就是为什么我以这种方式构造它的原因。 - CupawnTae

0

这里是一个移动追逐者的可用脚本:

function _( id ) { return document.getElementById( id ); }

window.addEventListener( 'load', function() {

 var targets = document.querySelectorAll('.target');
  var chaser = document.querySelector('#chaser');

 setTopPosition( targets );

function setTopPosition( targets ) {
    for (var i = 0; i < targets.length; i++) {
        targets[i].addEventListener('mouseenter', function(event) { 

            chaser.className = '';
            _( 'status' ).innerText = chaser.className; // to inspect the active class

           setTimeout(function() {
               /* at this point, I'm expecting no transition // to be active on the element */

              chaser.style.top = event.target.offsetTop + "px";
      }, 0); 

          // check if charser.className == ''
          if ( chaser.className == '') {
              setClassName();
          } else {
              alert( 0 );
          }

       }); // addEventListener
    } // for
} //function setTopPosition( targets )

function setClassName() {
    setTimeout(function() {

         /* at this point, I'm expecting the element to have finished moving to its new position */

        chaser.className = 'active';
        _( 'status' ).innerText = chaser.className;
         }, 0);
} // function setClassName()

});

HTML:

<div id="chaser">o</div> 

<div class="target">x</div> 
<div class="target">x</div> 
<div class="target">x</div> 
<div class="target">x</div> 
<div class="target">x</div>

<div id="status">status</div>

CSS:

#chaser { position: absolute; opacity: 0; }
#chaser.active { transition: all 1s; opacity: 1; }
.target { height: 30px; width: 30px; margin: 10px; background: #ddd; }

我不认为这回答了我的问题 - 我需要更多时间来阅读它。只是一个提醒 - 你可能不想在你的答案上选择“社区维基”。 - CupawnTae
我现在正在使用同样的代码,它正常工作。 - John Zenith
没错,但是正如我在问题中所说的,我已经用其他方法解决了这个问题,我特别询问的是样式更新的顺序。 (另外,FYI,在这里通常不认为仅有代码而没有解释的答案是完整的)。谢谢你的回答! - CupawnTae

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接