【问题标题】:Trigger CSS transition on appended element在附加元素上触发 CSS 过渡
【发布时间】:2022-01-11 06:30:04
【问题描述】:

正如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 动画?
  • 这些方法如何以及为何起作用?
  • 还有其他方法吗
  • 哪个(如果有)是首选解决方案?

【问题讨论】:

  • 好问题,由于脚本引擎优化了您的代码,因此动画可能迫在眉睫。

标签: javascript css dom


【解决方案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;
    }
    &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);
    }

    使用一次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】:

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

    (如果您严格不希望在初始节点上出现这些动画,请添加另一个类.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 属性时,初始动画将在元素“未悬停”后再次触发,这可能是不可取的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-08-20
    • 1970-01-01
    • 2011-10-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多