display: none 会彻底移除子元素渲染树节点,导致 transition 不启动、transitionend 不触发;应改用 visibility: hidden 配合 opacity 过渡,并在 transitionend 中校验 propertyName 后设 display: none。

父元素 display: none 会彻底卸载子元素,transition 不启动,transitionend 自然不触发
浏览器在遇到 display: none 时,会把整个子树从渲染树中移除——不是“隐藏”,是“注销”。子元素连样式计算都跳过,更别说过渡流程和事件派发。哪怕你给子元素写了 transition: opacity 0.3s,它也压根没机会进入 transition 状态。
常见错误现象:
- DevTools 的 Computed 面板里完全看不到子元素的
transition规则 - 父元素切回
display: block后再改子元素opacity,动画仍是“啪”一下出现,没有渐变
这不是卡顿或延迟,是浏览器主动跳过了整条流水线。
用 visibility: hidden + opacity 替代 display 才能保 transition 上下文
visibility: hidden 不破坏渲染树,子元素仍参与样式计算和 layout,transition 可正常启动。但要注意两点:
立即学习“前端免费学习笔记(深入)”;
-
visibility: hidden本身不可过渡,必须配合可过渡属性(如opacity)使用 - 动画结束后得手动补
display: none,否则元素继续占文档流、影响布局、干扰屏幕阅读器
典型节奏:visibility: visible; opacity: 1 → 过渡 → opacity: 0 → 监听 transitionend → 检查 e.propertyName === 'opacity' → 设 visibility: hidden; display: none。
transitionend 在父级显隐切换中漏触发,90% 是没监听对目标元素或属性
即使你用了 visibility 方案,transitionend 仍可能不触发,原因很具体:
- 监听了父元素,但事件实际由子元素发出:
e.target是子元素,不是父元素 - 多个属性同时过渡(比如
opacity和transform),但没校验e.propertyName,导致回调执行时机错乱 - 在
transitionend回调里又改了样式,却没先关掉transition,引发套娃过渡,新动画覆盖旧监听
正确做法:只监听子元素,且第一行就加 if (e.propertyName !== 'opacity') return;;回调中要改样式,先设 el.style.transition = 'none'。
真要用 display 控制显隐,JS 必须强制重排来“唤醒” transition
如果框架或遗留代码强依赖 display,又不能改样式结构,只能靠 JS 打断浏览器优化:
- 同步设
parent.style.display = 'block' - 立刻读取
parent.offsetHeight(不能用getComputedStyle,它不触发重排) - 再操作子元素的可过渡属性(如
child.style.opacity = '1')
这个 offsetHeight 读取是关键——它让浏览器同步重排,确认父元素已就位,子元素才能拿到有效起始状态。但慎用于滚动中频繁展开的场景,否则重排堆积引发卡顿。
真正容易被忽略的是:DOM 卸载时机与动画完成信号永远不同步。哪怕你用 visibility + opacity 实现了淡出,只要没在 transitionend 里检查 propertyName 并补 display: none,那个元素就一直挂着,占内存、拖重排、骗辅助技术。


















