在附加元素上触发 CSS 过渡

Posted

技术标签:

【中文标题】在附加元素上触发 CSS 过渡【英文标题】:Trigger CSS transition on appended element 【发布时间】:2014-07-31 15:41:03 【问题描述】:

正如this question 所观察到的,新附加元素上的即时 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 动画? 这些方法如何以及为何起作用? 还有其他方法吗 哪个(如果有)是首选解决方案?

【问题讨论】:

好问题,由于脚本引擎优化了您的代码,因此动画可能迫在眉睫。 【参考方案1】:

使用jQuery 试试这个(An Example Here.):

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 和 Internet Explorer 中的 runtimeStyle 属性)和不同的术语 浏览器用于某些属性。

您可以使用getComputedStyle()/css() 代替setTimeout。您也可以阅读this article 了解一些详细信息和示例。

【讨论】:

另一个很好的答案,+1 感谢您的赞许 :-) @AndreaLigios 我注意到 .css('opacity') 没有用 jquery 2.2 版本刷新 IE11 中的样式更改。相反 .offset() 在 IE11 中发挥了作用【参考方案2】:

没有为新添加的元素设置动画的原因是浏览器批量重排。

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

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

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

这确实是极少数情况之一,当这种行为是可取的。大多数情况下,在 DOM 修改之间访问导致回流的属性会导致严重的减速。

首选的解决方案可能因人而异,但对我来说,offsetWidth(或getComputedStyle())的访问是最好的。在某些情况下,setTimeout 在没有重新计算样式的情况下被触发。这是罕见的情况,主要是在加载的网站上,但它会发生。然后你不会得到你的动画。通过访问任何计算过的样式,您是在强制浏览器实际计算它。

【讨论】:

很好的答案,我希望我能 +2 “setTimeout 技巧有效,因为它会延迟类添加到另一个 javascript 轮次” 请注意,在 Firefox 上,setTimeout 持续时间很重要。我不知道细节,但对我(Firefox 42,Linux)来说,3ms 是它工作的最小值:jsfiddle.net/ca3ee5y8/4 @T.J.Crowder 看起来就像 Firefox 正在尝试进一步优化东西,如果在评估计时器回调之前没有绘制和回流,那么你就不走运了。这就是为什么最好知道如何同步触发它。如果您愿意,可以在回调的开头执行此操作。 @Frizi 很好的解释。我很好奇,是否有一些更深入的官方文档?也许在 MDN 上有什么? 看起来像在元素上调用getComputedStyle,即使将其分配给变量,也被 JIT 优化掉了。 (只有当我将它记录到控制台时,似乎才会发生回流。访问offsetWidth(即使没有使用)似乎也可以。【参考方案3】:

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

jQuery

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

Javascript

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

【讨论】:

这个问题说了这么多。我想了解为什么 这绝对是最好的方法。只需在 appendChild 之后添加一个“.focus()”就是一个不错且优雅的解决方案。【参考方案4】:

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

在 Chrome + Firefox 中,浏览器对渲染进行了太多优化,所以这仍然无济于事(适用于 Safari)。

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

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

我选择了 20 毫秒,因为它在 60 fps (16.7 毫秒) 时大于 1 帧,并且某些浏览器不会注册超时

应该强制动画开始进入下一帧,然后在浏览器准备再次绘制时负责任地启动它。

【讨论】:

是的。其他解决方案都不适合我。我发现的唯一方法是 setTimeout 的最小超时时间为 15 毫秒 进行两次嵌套的requestAnimationFrame 调用会更可靠吗? ?【参考方案5】:

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

假设我们有一个元素el,我们想要在其上转换opacity,但eldisplay:none; opacity: 0

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

// reflow
el.getBoundingClientRect();

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

【讨论】:

【参考方案6】:

编辑:原始答案中使用的技术,低于水平规则,在 100% 的情况下都不起作用,正如 mindplay.dk 在 cmets 中所指出的那样。

目前,如果使用requestAnimationFrame(),pomber's approach 可能是最好的,如 pomber 的答案中的article linked to 所示。这篇文章在 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;
&lt;script src="https://unpkg.com/afterframe/dist/afterframe.umd.js"&gt;&lt;/script&gt;

原答案:

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

【讨论】:

使用requestAnimationFrame() 对我有用;我在 4 年前遇到了同样的问题,我使用 $element.hide().show().addClass("visible") 强制重绘以使我的动画正常工作;这种方法要好得多! 对我来说,这似乎是随机工作的......我在页面加载时插入了一些 html,所以此时浏览器中有很多内容(加载/解析更多脚本、css、图像等)并且每第三或第四次刷新,动画就不会发生。我现在确定了两个嵌套的 requestAnimationFrame 调用,它完成了这项工作。 @mindplay.dk 你是对的。我设法让 sn-p 失败了几次(在 Firefox 中,但不是在 Chrome 中),所以我更新了答案。我很想知道我添加的任何一个东西在你的情况下是否可靠地工作(即使不是在生产中)? @DanL 原始答案中的方法在 100% 的情况下都不起作用,因此如果您仍在使用它,您可能需要重新评估这些选项。【参考方案7】:

我更喜欢 requestAnimationFrame + setTimeout(参见this post)。

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";
  )
);

试试here。

【讨论】:

为什么你更喜欢在 appendChild 行的末尾添加“.focus()”? parent.appendChild(child).focus();【参考方案8】:

setTimeout() 仅在 竞争条件 下起作用,应使用 requestAnimationFrame() 代替。但是offsetWidth 技巧是所有选项中效果最好的。

这是一个示例情况。我们有一系列的盒子,每个盒子都需要依次向下动画。为了让一切正常工作,我们需要为每个元素设置两次动画帧,这里我在动画之前和之后分别放置一次,但如果您将它们一个接一个地放置,它似乎也可以工作。

使用requestAnimationFrame 两次有效:

无论 2 个 getFrame()s 和单个 set-class-name 步骤的顺序如何精确地工作。

const delay = (d) => new Promise(resolve => setTimeout(resolve, d));
const getFrame = () => new Promise(resolve => window.requestAnimationFrame(resolve));

async function run() 
  for (let i = 0; i < 100; i++) 
    const box = document.createElement('div');
    document.body.appendChild(box);

    // BEFORE
    await getFrame();
    //await delay(1);

    box.className = 'move';
    
    // AFTER
    await getFrame();
    //await delay(1);
  


run();
div 
  display: inline-block;
  background-color: red;
  width: 20px;
  height: 20px;
  
  transition: transform 1s;


.move 
  transform: translate(0px, 100px);

使用setTimeout 两次失败:

由于这是基于竞争条件的,因此具体结果会因您的浏览器和计算机而有很大差异。增加setTimeout 延迟有助于动画更频繁地赢得比赛,但不能保证。

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

const delay = (d) => new Promise(resolve => setTimeout(resolve, d));
const getFrame = () => new Promise(resolve => window.requestAnimationFrame(resolve));

async function run() 
  for (let i = 0; i < 100; i++) 
    const box = document.createElement('div');
    document.body.appendChild(box);

    // BEFORE
    //await getFrame();
    await delay(1);

    box.className = 'move';
    
    // AFTER
    //await getFrame();
    await delay(1);
  


run();
div 
  display: inline-block;
  background-color: red;
  width: 20px;
  height: 20px;
  
  transition: transform 1s;


.move 
  transform: translate(0px, 100px);

使用一次requestAnimationFramesetTimeout 通常有效:

这是Brendan's solution(setTimeout 第一个)或pomber's solution(requestAnimationFrame 第一个)。

# works:
getFrame()
delay(0)
ANIMATE

# works:
delay(0)
getFrame()
ANIMATE

# works:
delay(0)
ANIMATE
getFrame()

# fails:
getFrame()
ANIMATE
delay(0)

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

const delay = (d) => new Promise(resolve => setTimeout(resolve, d));
const getFrame = () => new Promise(resolve => window.requestAnimationFrame(resolve));

async function run() 
  for (let i = 0; i < 100; i++) 
    const box = document.createElement('div');
    document.body.appendChild(box);

    // BEFORE
    await getFrame();
    await delay(1);

    box.className = 'move';
    
    // AFTER
    //await getFrame();
    //await delay(1);
  


run();
div 
  display: inline-block;
  background-color: red;
  width: 20px;
  height: 20px;
  
  transition: transform 1s;


.move 
  transform: translate(0px, 100px);

【讨论】:

setTimeout() works only due to race conditions:我认为这不是真的,setTimeout() 之所以有效,是因为它将您传递的函数推送到回调队列中,然后偶数循环等待直到当前调用堆栈被清空,并且然后它将函数推入调用堆栈。【参考方案9】:

将关键帧用于“创建动画”有什么根本错误吗?

(如果您严格不希望在初始节点上出现这些动画,请添加另一个类.initialinhibitin 动画)

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>

【讨论】:

虽然它有一些缺陷。例如,当您在div:hover 中覆盖animation 属性时,初始动画将在元素“未悬停”后再次触发,这可能是不可取的。

以上是关于在附加元素上触发 CSS 过渡的主要内容,如果未能解决你的问题,请参考以下文章

CSS3 过渡以突出显示在 JQuery 中创建的新元素

如何解释 D3 附加到 DOM 元素的 __transition__ 属性作为过渡的一部分?

将元素附加到子元素会触发父元素中的 drop 事件

CSS3之过渡

逐步淡入无限滚动附加项目

CSS3使用JAVASCript触发过渡效果