触发附加元素的CSS转换

时间:2014-06-10 18:31:28

标签: javascript css css3 dom

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

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

编辑 - 我正在使用Chrome 35。

JSFiddle(包括vanilla JS示例)。

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

10 个答案:

答案 0 :(得分:97)

不动画新添加元素的原因是浏览器批量重排。

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

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

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

这是非常少的场合之一,当这种行为是可取的时候。大多数时候,在DOM修改之间访问引起回流的属性会导致严重的减速。

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

答案 1 :(得分:27)

使用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了解一些详细信息和示例。

答案 2 :(得分:6)

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

答案 3 :(得分:5)

请使用以下代码,使用“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';

答案 4 :(得分:2)

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

在Chrome + Firefox中,浏览器会过度优化渲染,因此这仍然无法提供帮助(适用于Safari)。

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

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

我选择了20ms,因为它在60fps(16.7ms)时大于1帧,有些浏览器不会注册超时<5ms。

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

答案 5 :(得分:2)

setTimeout()仅由于比赛条件而起作用,应改为使用requestAnimationFrame()。但是offsetWidth技巧在所有选项中效果最好。

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

两次使用requestAnimationFrame

无论2 getFrame()和单个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(首先是setTimeoutpomber'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);
}

答案 6 :(得分:0)

Brendan不同,我发现requestAnimationFrame()适用于Chrome 63,Firefox 57,IE11和Edge。

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

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

.fade {
  opacity: 0;
  transition: opacity 2s;
}
&#13;
&#13;
&#13;

答案 7 :(得分:0)

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

npm cache clean --force

here试试。

答案 8 :(得分: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>

答案 9 :(得分:0)

要继续代码行流,只需在函数声明前添加 async 并添加

await new Promise(resolve=>{setTimeout(resolve, 10)})

在附加孩子之后。它将暂停 10ms 的代码执行,然后其余代码按原样正常执行,无需修改任何内容或包装到超时函数。