transition无法作用于display:none的元素,因其会立即移除元素;应改用height/max-height/opacity等可过渡属性分步实现动画,并在react中通过中间状态和transitionend事件精准控制dom移除时机。

transition 无法作用于 display: none 的元素
CSS transition 不会触发当目标属性被设为 display: none —— 因为这个值会让元素立刻从渲染树中移除,动画根本没机会执行。常见现象是:列表项点击删除后“啪”一下消失,毫无收缩过程。
正确做法是分两步走:先触发动画(比如高度、opacity),等动画结束再真正移除 DOM。关键在于避免直接操作 display,改用可过渡的属性:
-
height+overflow: hidden:设初始height为具体值,过渡到0 -
max-height:比height更容错(不用精确知道内容高度),设一个足够大的上限值(如max-height: 100vh)再过渡到0 -
opacity配合transform: scaleY():视觉上更自然,但需注意transform不影响文档流
React 中用 useEffect 监听删除状态并控制 class 切换
在 React 里,不能靠 useState 更新就自动加动画;必须显式管理「正在退出」这个中间状态。典型错误是直接在 onClick 里调用 setItems(items.filter(...)),导致组件瞬间卸载。
实操建议:
立即学习“前端免费学习笔记(深入)”;
- 给每项加独立的
isDeleting状态(或用 Map 记录 id → deleting) - 点击删除时只更新该状态,不立即删数据;等
transitionend事件触发后再真正从列表中剔除 - 用
useEffect监听isDeleting变化,自动添加deletingclass - CSS 里写
.item.deleting { height: 0; opacity: 0; }和对应transition
示例关键片段:
const [deletingIds, setDeletingIds] = useState(new Set());<br>const handleDelete = (id) => {<br> setDeletingIds(prev => new Set([...prev, id]));<br> setTimeout(() => {<br> setItems(items.filter(item => item.id !== id));<br> setDeletingIds(prev => {<br> const next = new Set(prev);<br> next.delete(id);<br> return next;<br> });<br> }, 300); // 匹配 transition-duration
transitionend 事件监听要防多次触发和内存泄漏
一个 DOM 元素可能同时有多个可过渡属性(height、opacity、transform),transitionend 会为每个属性各触发一次。常见错误是没校验 event.propertyName,导致回调执行多次,甚至误删还没动画完的其他项。
安全写法:
- 监听时加
{ once: true },确保只响应一次 - 在回调里检查
event.propertyName === 'height'或你关心的那个属性名 - 如果用 ref 绑定事件,
useEffect清理函数里记得removeEventListener - 别在列表循环里直接写
el.addEventListener,容易绑定错对象或漏清理
移动端 Safari 对 height 过渡的支持不稳定
iOS Safari 在某些版本里对 height: 0 过渡有渲染 bug:动画中途卡住、闪回、或直接跳变。这不是你代码写错了,是浏览器限制。
更稳妥的替代方案:
- 优先用
max-height替代height,它在 Safari 中兼容性更好 - 避免用
height: auto过渡 —— 浏览器无法计算 auto 的起止值,动画会失效 - 如果必须用
transform,注意scaleY(0)后元素仍占空间,得配合overflow: hidden或父级height: 0控制 - 测试时务必真机验证,模拟器不一定复现
最麻烦的地方其实是:动画结束时机和 DOM 移除的耦合点,稍一错位就会残留空白、错乱重排、或者动画被中断。这个节奏得靠事件、延时、状态三者对齐,不是加个 transition 就完事。










