在附加元素上触发 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
,但el
是display: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;
<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;
【讨论】:
使用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);
使用一次requestAnimationFrame
和setTimeout
通常有效:
这是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】:
将关键帧用于“创建动画”有什么根本错误吗?
(如果您严格不希望在初始节点上出现这些动画,请添加另一个类.initial
inhibitin 动画)
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 过渡的主要内容,如果未能解决你的问题,请参考以下文章