在附加元素上触发CSS过渡


73

正如这个问题所观察到的,新添加的元素上的CSS过渡立即被忽略了-过渡的结束状态立即被呈现。

例如,给定此CSS(此处省略前缀):

.box { 
  opacity: 0;
  transition: all 2s;
  background-color: red;
  height: 100px;
  width: 100px;
}

.box.in { opacity: 1; }

此元素的不透明度将立即设置为1:

// Does not animate
var $a = $('<div>')
    .addClass('box a')
    .appendTo('#wrapper');
$a.addClass('in');

我已经看到了触发过渡以获得预期行为的几种方法:

// Does animate
var $b = $('<div>')
    .addClass('box b')
    .appendTo('#wrapper');

setTimeout(function() {
    $('.b').addClass('in');
},0);

// Does animate
var $c = $('<div>')
    .addClass('box c')
    .appendTo('#wrapper');

$c[0]. offsetWidth = $c[0].offsetWidth
$c.addClass('in');

// Does animate
var $d = $('<div>')
    .addClass('box d')
    .appendTo('#wrapper');
$d.focus().addClass('in');

相同的方法适用于原始JS DOM操作-这不是jQuery特定的行为。

编辑-我正在使用Chrome 35。

JSFiddle(包括香草JS示例)。

  • 为什么忽略附加元素上的即时CSS动画?
  • 这些方法如何以及为什么起作用?
  • 还有其他方法吗
  • 哪个是首选解决方案?

很好的问题,由于脚本引擎优化了代码,动画即将到来。
Etheryte 2014年

Answers:


119

无法激活新添加的元素的动画的原因是浏览器的批量重排。

添加元素时,需要回流。添加类也是如此。但是,当您在单个javascript循环中同时执行这两个操作时,浏览器会抓住机会优化第一个。在那种情况下,只有单个(同时是初始和最终)样式值,因此将不会发生过渡。

setTimeout技巧行之有效,因为它会延迟将类添加到另一轮javascript循环中,因此呈现引擎需要呈现两个值,因为有一个时间点,第一个呈现给用户时需要计算。

批处理规则还有另一个例外。如果您尝试访问即时值,浏览器需要计算它。这些值之一是offsetWidth。当您访问它时,将触发重排。在实际显示期间,另外一个是单独完成的。同样,我们有两个不同的样式值,因此我们可以及时对它们进行插值。

实际上,这是极少数需要这种行为的情况之一。在大多数情况下,在DOM修改之间访问引起回流的属性可能会导致严重的速度下降。

首选的解决方案可能因人而异,但对我而言,offsetWidth(或getComputedStyle())的访问权限是最佳的。在某些情况下,如果setTimeout在不进行样式重新计算的情况下将其触发。这种情况很少见,大多数情况是在已加载的网站上发生的,但它确实发生了。这样一来,您将无法获得动画。通过访问任何计算出的样式,您将强制浏览器进行实际计算。


11
好的答案,我希望我能+2
Andrea Ligios 2014年

2
“ setTimeout技巧很有用,因为它会延迟类添加到另一轮javascriptsetTimeout时间。请注意,在Firefox上,持续时间很重要。我不知道细节,但是对我来说(Firefox 42,Linux),最少要3毫秒
TJ Crowder

@TJCrowder看起来像是Firefox正在尝试进一步优化内容,如果在评估计时器回调之前没有油漆和回流,那么您很不走运。因此,最好知道如何同步触发它。如果愿意,可以在回调的开头进行操作。
Frizi

@Frizi很好的解释。我很好奇,是否有一些更深入的官方文档?可能是MDN上的东西?
罗勒姆

2
看起来getComputedStyle,即使在将元素分配给变量时,JIT也对其进行了优化。(仅当我将其登录到控制台时,似乎才发生重排。访问offsetWidth(即使不使用)也似乎可行
。– Kissaki

30

使用jQuery这个(这里是一个例子):

var $a = $('<div>')
.addClass('box a')
.appendTo('#wrapper');
$a.css('opacity'); // added
$a.addClass('in');

使用Vanilla javaScript尝试以下操作:

var e = document.createElement('div');
e.className = 'box e';
document.getElementById('wrapper').appendChild(e);
window.getComputedStyle(e).opacity; // added
e.className += ' in';

简要说明:

的getComputedStyle()刷新所有悬而未决的风格变化,迫使布局引擎来计算元素的当前状态,因此 的.css()的工作原理类似的方式。

关于css()jQuery网站:

.css()方法是从第一个匹配的元素获取样式属性的便捷方法,尤其是考虑到浏览器访问大多数属性的方式不同(基于标准的浏览器中的getComputedStyle()方法与currentStyle和runtimeStyle属性),以及浏览器对某些属性使用的不同术语。

您可以使用getComputedStyle()/css()代替setTimeout。您也可以阅读本文以获得一些详细信息和示例。


1
另一个不错的答案,+ 1
Andrea Ligios 2014年

1
感谢您的赞许:-) @AndreaLigios
Alpha

我注意到.css('opacity')并未使用jquery 2.2版刷新IE11中的样式更改。相反.offset()的确在IE11的伎俩
马克Steggles

10

请使用以下代码,使用“ focus()”

jQuery的

var $a = $('<div>')
    .addClass('box a')
    .appendTo('#wrapper');
$a.focus(); // focus Added
$a.addClass('in');

Java脚本

var e = document.createElement('div');
e.className = 'box e';
document.getElementById('wrapper').appendChild(e).focus(); // focus Added
e.className += ' in';

2
问题这么多。我想知道为什么
joews 2014年

这是绝对最佳的方法。仅在appendChild之后添加“ .focus()”是一个很好的解决方案。
Daniele Testa,

7

@Frizi的解决方案有效,但是有时我发现getComputedStyle在更改元素上的某些属性时这种方法无效。如果这不起作用,您可以尝试getBoundingClientRect()以下操作,我发现它是防弹的:

假设我们有一个el要在其上过渡的element opacity,但是eldisplay:none; opacity: 0

el.style.display = 'block';
el.style.transition = 'opacity .5s linear';

// reflow
el.getBoundingClientRect();

// it transitions!
el.style.opacity = 1;

5

我更喜欢requestAnimationFrame + setTimeout(请参阅这篇文章)。

const child = document.createElement("div");
child.style.backgroundColor = "blue";
child.style.width = "100px";
child.style.height = "100px";
child.style.transition = "1s";

parent.appendChild(child);

requestAnimationFrame(() =>
  setTimeout(() => {
    child.style.width = "200px";
  })
);

在这里尝试。


为什么比在appendChild行的末尾简单添加“ .focus()”更好呢?parent.appendChild(child).focus();
Daniele Testa

2

我没有尝试强制立即重新绘制或计算样式,而是尝试使用 requestAnimationFrame()允许浏览器在其下一个可用框架上绘画,绘画。

在Chrome + Firefox中,浏览器过多地优化了渲染,因此仍然无济于事(在Safari中可以使用)。

我决定手动强制延迟,setTimeout()然后使用requestAnimationFrame()负责任地让浏览器绘制。如果在超时结束之前未绘制附件,则动画可能会被忽略,但它似乎可以可靠地工作。

setTimeout(function () {
    requestAnimationFrame(function () {
        // trigger the animation
    });
}, 20);

我选择20毫秒是因为它在60帧/秒(16.7毫秒)时大于1帧,并且某些浏览器不会将超时记录为<5毫秒。

手指交叉将迫使动画开始进入下一帧,然后在浏览器准备再次绘制时负责任地开始动画。


是。没有其他解决方案对我有用。我发现的唯一方法是setTimeout的最小超时时间为15ms
Yashas

进行两个嵌套requestAnimationFrame调用会更可靠吗?🤔–
mindplay.dk

2

setTimeout()只能因竞争条件requestAnimationFrame()应改为使用。但是offsetWidth,在所有选项中,这种技巧最有效。

这是一个示例情况。我们有一系列的框,每个框需要按顺序向下动画。为了使一切正常工作,我们需要为每个元素获取两次动画帧,这里我在动画之前放置了一次,在动画之后放置了一次,但是如果将它们一个接一个放置,这似乎也可以工作。

使用requestAnimationFrame两次有效:

无论2 getFrame()s和单个set-class-name步骤的排序顺序如何,都可以使用。

使用setTimeout两次失败:

由于这是基于比赛条件的,因此确切的结果会因您的浏览器和计算机而相差很大。增加setTimeout延迟会帮助动画更频繁地赢得比赛,但不能保证任何效果。

在我的Surfacebook 1上使用Firefox,并且延迟2ms / el,我发现大约50%的盒子出现故障。以20ms / el的延迟,我看到大约10%的包装盒失效。

通常使用requestAnimationFrame一次即可setTimeout

这是布伦丹的解决方案setTimeout第一)或庞伯的解决方案requestAnimationFrame第一)。

# works:
getFrame()
delay(0)
ANIMATE

# works:
delay(0)
getFrame()
ANIMATE

# works:
delay(0)
ANIMATE
getFrame()

# fails:
getFrame()
ANIMATE
delay(0)

一次不起作用的情况(对我而言)是在获取帧时进行动画处理,然后进行延迟。我没有解释为什么。


1

编辑:水平答案下方的原始答案中使用的技术无法100%地起作用,如mindplay.dk的注释中所述

当前,如果使用requestAnimationFrame(),则pomber的方法可能是最好的,如从pomber的答案中链接文章中可以看出。自从pomber回答以来,该文章已进行了更新,现在提到了requestPostAnimationFrame(),该标记现在位于Chrome标志--enable-experimental-web-platform-features后面。

requestPostAnimationFrame()在所有主要浏览器中达到稳定状态时,这大概可以可靠地工作:

const div = document.createElement("div");
document.body.appendChild(div);

requestPostAnimationFrame(() => div.className = "fade");
div {
  height: 100px;
  width: 100px;
  background-color: red;
}

.fade {
  opacity: 0;
  transition: opacity 2s;
}

但是,暂时有一个名为AfterFrame的polyfill,在前面的文章中也有引用。例:

const div = document.createElement("div");
document.body.appendChild(div);

window.afterFrame(() => div.className = "fade");
div {
  height: 100px;
  width: 100px;
  background-color: red;
}

.fade {
  opacity: 0;
  transition: opacity 2s;
}
<script src="https://unpkg.com/afterframe/dist/afterframe.umd.js"></script>


原始答案:

Brendan不同,我发现它可以requestAnimationFrame()在Chrome 63,Firefox 57,IE11和Edge中工作。

var div = document.createElement("div");
document.body.appendChild(div);

requestAnimationFrame(function () {
  div.className = "fade";
});
div {
  height: 100px;
  width: 100px;
  background-color: red;
}

.fade {
  opacity: 0;
  transition: opacity 2s;
}


1
requestAnimationFrame()为我使用作品;我在4年前遇到了同样的问题,在该问题中,$element.hide().show().addClass("visible")为了使动画能够正常工作,我不得不重新绘画。这种方法好多了!
丹L

1
对我来说,这似乎是随机工作的...我是在页面加载时插入一些HTML,因此此时浏览器中有很多工作(加载/解析更多脚本,css,图像等),每三分之一或第四次刷新,动画不会发生。我现在解决了两个嵌套的requestAnimationFrame调用,完成了任务。
mindplay.dk

@ mindplay.dk你是对的。我设法使代码段多次失败(在Firefox中,但在Chrome中不是),所以我更新了答案。我想知道我添加的任何一种方法是否在您的情况下都能可靠地工作(即使不在生产中)?
詹姆斯·T

@DanL原始答案中的方法在100%的时间内无效,因此,如果您仍在使用此方法,则可能需要重新评估选项。
詹姆斯·T

0

使用关键帧进行“创建时动画”有什么根本错误?

(如果您严格不要在初始节点上使用这些动画,请在动画中添加另一个类.initial抑制)

function addNode() {
  var node = document.createElement("div");
  var textnode = document.createTextNode("Hello");
  node.appendChild(textnode);

  document.getElementById("here").appendChild(node);  
}

setTimeout( addNode, 500);
setTimeout( addNode, 1000);
body, html { background: #444; display: flex; min-height: 100vh; align-items: center; justify-content: center; }
button { font-size: 4em; border-radius: 20px; margin-left: 60px;}

div {
  width: 200px; height: 100px; border: 12px solid white; border-radius: 20px; margin: 10px;
  background: gray;
  animation: bouncy .5s linear forwards;
}

/* suppres for initial elements */
div.initial {
  animation: none;
}

@keyframes bouncy {
  0% { transform: scale(.1); opacity: 0 }  
  80% { transform: scale(1.15); opacity: 1 }  
  90% { transform: scale(.9); }  
  100% { transform: scale(1); }  
}
<section id="here">
  <div class="target initial"></div>
</section>

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.