【问题标题】:setTimeout(fn,0) triggering before styles have been appliedsetTimeout(fn,0) 在应用样式之前触发
【发布时间】:2017-02-14 14:49:45
【问题描述】:

当我想在继续之前让我的 javascript 执行并允许浏览器应用样式等时,我倾向于使用setTimeout 的常用技术,延迟为0 让回调在队列中排队事件循环结束。但是,我遇到了一种情况,这似乎不能可靠地工作。

在下面的 sn-p 中,我有一个 active 类,它将转换应用于 chaser 元素。

当我将鼠标悬停在目标 div 上时,我想从 chaser 元素中删除 active 类,将 chaser 移动到新位置,然后重新应用 active 类。效果应该是o 应该立即消失,然后淡入它的新位置。相反,opacitytop 都应用了过渡,所以o 在不同位置滑动大部分时间

如果我将内部超时的延迟增加到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>

【问题讨论】:

  • 如果您使用 requestAnimationFrame 而不是 setTimeout(fn,0) 会怎样。如果您执行双重 requestAnimationFrame,那么我认为浏览器应该完成自我更新。
  • @David 可能会起作用,但不确定它是否能提供任何保证,但我会做一些阅读。我更担心setTimeout 没有达到我的预期,因为我过去一直依赖它,现在我想知道还有什么可能被破坏
  • @David 对单个 requestAnimationFrame 的初始实验表明它在大多数情况下都有效,但并非总是如此。我猜第二个requestAnimationFrame 可能会实现一致性,但我认为这可能只是因为你基本上延迟了足够长的时间来避免竞争条件——我真的很喜欢确定性的东西......

标签: javascript css settimeout


【解决方案1】:

在做任何其他事情之前,你需要听的是transitionend 事件。你可以阅读MDN about transitionend event。顺便说一句,setTimeout 永远不应该用来保证时间。

编辑:这是在 OP 澄清后供参考。每当元素发生样式更改时,都会进行重排和/或重绘。你可以read more about them here。如果第二次 setTimeout 在第一次回流之前运行,那么您将获得滑动效果。 10ms 之所以会达到预期效果是因为.active 类是在offsetTop 属性调整后添加的(导致transition 属性在元素更改为offsetTop 后应用)。通常,有 60 fps(即:每帧约 16 毫秒),这意味着在应用新样式之前您有 16 毫秒的窗口可以做任何事情。这就是为什么 5ms 的小延迟有时会导致不同的结果。

TL:DR - 浏览器每 16 毫秒询问 JS 和 CSS 是否有任何更新,并计算要绘制的内容。如果你错过了 16 毫秒的窗口,你可能会得到完全不同的结果。

【讨论】:

  • 谢谢,但我不想等待过渡 - 如果它正在进行中,我想在中间中止它,这实际上工作正常,这是第二部分的行为奇怪,并且有在那种情况下应该没有过渡
  • 那是因为你在 setTimeout 中有一个 setTimeout。第二个 setTimeout 只会被添加到执行循环中,只有在第一个已经运行后,并且只有在 JS 主循环清空后才会运行。
  • 但这正是我想要发生的。事实上,我最初没有外部setTimeout - 我做了remove class并一步更改top,然后做了一个setTimeout重新添加active class。我添加了额外的步骤以防万一它导致问题,但事实并非如此。
  • 您只是想避免sliding in 效应吗?即:如果我将鼠标悬停在目标上,o 就会出现?
  • 我不只是想要那个 - 正如我在问题中所说的,我可以通过仅将转换应用到 opacity 来做到这一点。我想了解为什么会发生这种情况,特别是如果/为什么 setTimeout 在这里不可靠,以及是否有办法保证应用样式的顺序
【解决方案2】:

您正在另一个 setTimeout() 计时方法中调用 setTimeout() 计时方法。

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

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

【讨论】:

  • 我看不出您的建议与我的建议有何不同 - 第二个 setTimeout 发生在第一个 setTimeout 的回调执行结束时
  • 好吧,我想说的是,单独调用两个 setTimeout() 方法会更好。
  • 如果你一个接一个地调用它们,它会将两个回调都放在事件循环队列的末尾,然后它们将按顺序执行,而不会在其间运行另一个事件循环。我的目标是让事件循环在第一个和第二个回调之间运行(并更新样式,不幸的是没有发生),这就是我按照我的方式构建它的原因。
【解决方案3】:

这是一个移动追逐者的工作脚本:

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; }

【讨论】:

  • 我真的不认为这能回答我的问题 - 我需要更多时间来通读它。不过请注意 - 您可能不想在答案中选择“社区 wiki”
  • 我现在正在使用相同的代码,并且工作正常。
  • 对,但正如我在问题中所说,我已经可以通过其他方式解决问题,我专门询问样式更新的顺序。 (另外,仅供参考,一般来说,没有解释的纯代码答案在这里不被认为是完整的)。不过感谢您的意见!
猜你喜欢
  • 2014-07-29
  • 1970-01-01
  • 1970-01-01
  • 2021-08-13
  • 1970-01-01
  • 1970-01-01
  • 2021-10-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多