display:none转block后,jQuery触发CSS transform失效,需延迟执行
问题
我有一个默认通过CSS设置为display:none;隐藏的元素,用jQuery将其设为可见:
$('#element-id').css('display','block');
紧接着给父元素添加CSS类触发变换:
$('#parentdiv').addClass('trigger-animation');
但浏览器好像还没处理完内联CSS的渲染(把元素设为可见)就应用了变换,导致动画不生效。把addClass放进setTimeout(function(){ $('#parentdiv').addClass('trigger-animation'); },25);甚至0ms延迟就能正常工作,我想知道原因,还要找更可靠的跨浏览器方案——怎么判断浏览器已经完成display:block;的渲染?
以下是最小复现示例:
<!DOCTYPE html> <html lang="de"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ProFuel mobiles menü</title> <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script> <style> * { box-sizing: border-box; } body { margin: 0; padding: 0; font-family: sans-serif; } #mblnv { position: relative; width: 250px; height: 100vh; background: #852470; /*overflow-x: hidden;*/ margin-left: auto; margin-right: auto; } #mblnv ul { margin: 0; padding: 0; } #mblnv ul li { list-style: none; } ul.mblnv-lvl-1 a.mblnv-heading { font-weight: 700; } .mblnv-lvl-0{ display: none; position: absolute; top: 0; width: 100%; background: #852470; } .mblnv-lvl-0 { left: 0; transition: transform 1s cubic-bezier(0.165, 0.84, 0.44, 1) 0.1s; } .mblnv-lvl-0 > li > a { opacity: 0; transform: translateY(100px); display: block; padding: 8px; border-bottom: 1px solid #600f4e; font-size: 14px; font-weight: 400; line-height: 20px; color: #fff; text-decoration: none; text-transform: uppercase; } .mblnv-lvl-0-open .mblnv-lvl-0 > li > a { opacity: 1; transform: translateY(0); } #helper { background: orange; position: absolute; width: 100px; top: 0; right: 0; z-index: 1000; } #helper a{ color: white; text-align: center; } .mblnv-transition-style-style-3{transition:transform 1s cubic-bezier(0.165, 0.84, 0.44, 1) 0.28s,opacity 1s cubic-bezier(0.165, 0.84, 0.44, 1) 0.37s;} .mblnv-transition-style-style-2{transition:transform 1s cubic-bezier(0.165, 0.84, 0.44, 1) 0.21s,opacity 1s cubic-bezier(0.16, 0.85, 0.45, 1) 0.30s;} .mblnv-transition-style-style-1{transition: transform 1s cubic-bezier(0.1, 0.85, 0.45, 1) 0.1s, opacity 1s cubic-bezier(0.24, 0.88, 0.53, 1) 0.2s;} </style> </head> <body> <nav id="mblnv"> <ul class="mblnv-lvl-0"> <li> <a href="#" class="mblnv-animation-slide-up-reveal mblnv-transition-style-style-1">Menu Item 1</a> </li> <li> <a href="#" class="mblnv-lvl-0-to-lvl-1-link mblnv-animation-slide-up-reveal mblnv-transition-style-style-2">Menu Item 2</a> </li> <li> <a href="#" class="mblnv-animation-slide-up-reveal mblnv-transition-style-style-3">Menu Item 3</a> </li> </ul> </nav> <div id="helper"> <a href="#" id="nav-open">Nav open no animation</a><br><br> <a href="#" id="nav-open-2">Nav open with animation</a> </div> <script> // no animation $('#nav-open').click(function(){ $('.mblnv-lvl-0').css('display','block'); $('#mblnv').addClass('mblnv-lvl-0-open'); }); // animation $('#nav-open-2').click(function(){ $('.mblnv-lvl-0').css('display','block'); setTimeout(function(){ $('#mblnv').addClass('mblnv-lvl-0-open'); },0); }); </script> </body> </html>
为什么setTimeout(0)能解决问题?
浏览器的渲染引擎和JS引擎互斥,同一时间只能运行其中一个。连续执行css('display','block')和addClass时,这两个操作属于JS线程的同步任务,会被一次性执行完毕。浏览器要等JS线程空闲后,才会计算元素新样式、重新渲染页面。
而setTimeout(fn, 0)会把addClass放进宏任务队列,等当前JS同步代码执行完、浏览器完成一次渲染后,再执行这个任务。这就给了浏览器足够时间把元素从display:none改为block,后续添加类触发的变换就能正常启动过渡动画。
更可靠的跨浏览器解决方案
方案1:使用requestAnimationFrame
比setTimeout(0)更精准,它会在浏览器下一次重绘前执行回调,确保元素已经完成渲染:
$('#element-id').css('display','block'); requestAnimationFrame(function() { $('#parentdiv').addClass('trigger-animation'); });
主流浏览器均支持,兼容性比硬写固定延迟更可靠。
方案2:用CSS类控制显示(推荐)
把显示/隐藏逻辑也交给CSS类实现,避免JS直接操作样式:
.mblnv-lvl-0 { display: none; /* 原有样式 */ } .mblnv-lvl-0.show { display: block; }
JS中先添加显示类,再用requestAnimationFrame添加动画触发类:
$('.mblnv-lvl-0').addClass('show'); requestAnimationFrame(function() { $('#mblnv').addClass('mblnv-lvl-0-open'); });
这种方式符合「样式由CSS控制」的最佳实践,减少JS与CSS的耦合。
方案3:读取布局属性强制重绘
浏览器读取元素的某些布局属性(如offsetHeight、getBoundingClientRect())时,会强制触发重绘计算,确保当前样式生效:
$('#element-id').css('display','block'); // 读取属性触发重绘 $('#element-id')[0].offsetHeight; $('#parentdiv').addClass('trigger-animation');
该方法同步执行,无需等待异步任务,但频繁读取可能影响性能,单次使用无问题。
内容的提问来源于stack exchange,提问作者endevvour-rs

