4、Transition组件详解:掌控动画状态
Transition组件,说白了就是React Transition Group里最核心的那个家伙。我刚开始用的时候也觉得它有点绕,但用顺手了就会发现——它给了你一把控制动画状态的钥匙。
你想想看,一个元素从出现到消失,中间经历了什么?entering → entered → exiting → exited,这四个状态就是Transition组件的灵魂。搞懂了它们,你就能精准控制动画的每一帧。
4.1 四个核心状态
Transition组件把动画生命周期切成了四段。每一段你都可以挂载不同的样式或行为。
| 状态 | 含义 | 触发时机 |
|---|---|---|
entering |
进入中 | 组件挂载或in变为true时立即触发 |
entered |
进入完成 | entering结束后触发 |
exiting |
退出中 | in变为false时立即触发 |
exited |
退出完成 | exiting结束后触发 |
关键点:entering和exiting是过渡态,entered和exited是稳定态。动画效果通常挂在过渡态上。
举个例子。我做一个弹窗,希望它出现时从透明变不透明,消失时反过来。代码可以这样写:
import { Transition } from 'react-transition-group';
const duration = 300;
const defaultStyle = {
transition: `opacity ${duration}ms ease-in-out`,
opacity: 0,
};
const transitionStyles = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 },
};
function Fade({ in: inProp }) {
return (
<Transition in={inProp} timeout={duration}>
{state => (
<div style={{
...defaultStyle,
...transitionStyles[state]
}}>
我出现了
</div>
)}
</Transition>
);
}
嗯,这里要注意——entering和entered的样式可以一样,但它们的语义完全不同。前者是过渡中,后者是已稳定。我在项目中见过有人把动画逻辑全塞在entered里,结果动画根本不执行。为什么?因为entered是最终态,CSS transition需要从entering到entered的变化过程才能触发。
4.2 自定义动画时长与延迟
Transition组件默认用timeout控制动画时长。但实际项目中,你往往需要更精细的控制。
timeout可以传一个数字,也可以传一个对象:
<Transition
in={inProp}
timeout={{
enter: 500,
exit: 300,
}}
>
{state => (/* ... */)}
</Transition>
这样进入动画500ms,退出动画300ms。我个人习惯把进入动画做得比退出长一点,因为用户更关注内容出现的过程,退出时快一点反而感觉干脆。
那延迟呢?比如我想让元素在进入前先等200ms再开始动画。可以用onEntering回调配合setTimeout,但更优雅的方式是用CSS的animation-delay或transition-delay。
const defaultStyle = {
transition: `opacity 500ms ease-in-out 200ms`, // 200ms延迟
opacity: 0,
};
小技巧:如果你需要多个元素依次出现(比如列表动画),可以给每个元素递增的transition-delay。我曾经用这个技巧做了一个卡片瀑布流效果,用户体验非常好。
4.3 回调函数:精准控制动画节点
Transition组件提供了四个回调:onEnter、onEntering、onEntered、onExit、onExiting、onExited。它们分别在状态切换时触发。
举个例子,我想在动画结束后执行一些清理工作:
<Transition
in={inProp}
timeout={300}
onEntered={() => console.log('进入完成')}
onExited={() => console.log('退出完成,可以卸载了')}
>
{state => (/* ... */)}
</Transition>
避坑指南:我曾经在onExited里直接调用了setState来移除DOM元素,结果造成了无限循环。因为setState导致组件重新渲染,又触发了in的变化。正确的做法是:在onExited里只做标记,由父组件控制in的值。
4.4 appear属性:首次渲染也动画
默认情况下,Transition组件只在in从false变为true时触发进入动画。如果组件首次挂载时in就是true,它不会执行entering,直接跳到entered。
想让首次渲染也有动画?加个appear属性:
<Transition
in={true}
timeout={300}
appear
>
{state => (/* 首次也会执行entering */)}
</Transition>
这个属性我经常用在页面加载时的欢迎动画上。用户第一次看到页面时,元素从底部滑入,感觉非常自然。
4.5 实际项目中的组合用法
说了这么多,咱们来一个综合例子。一个可关闭的提示条,进入时从右侧滑入,退出时向左滑出,并且有延迟效果。
function AlertBanner({ show, onClose }) {
return (
<Transition
in={show}
timeout={{
enter: 400,
exit: 200,
}}
unmountOnExit
>
{state => (
<div
style={{
transition: 'transform 400ms ease-in-out',
transform: state === 'entering' || state === 'entered'
? 'translateX(0)'
: 'translateX(100%)',
}}
>
这是一条提示
<button onClick={onClose}>关闭</button>
</div>
)}
</Transition>
);
}
这里用了unmountOnExit,退出动画结束后DOM自动移除。省去了手动管理组件卸载的麻烦。
总结一下:
- 四个状态:entering、entered、exiting、exited
- timeout支持对象形式,分别控制进入和退出时长
- 延迟用CSS transition-delay实现,更灵活
- 回调函数用于副作用,但小心别造成循环
- appear让首次渲染也有动画
Transition组件其实不复杂,但它是所有高级动画组件的基础。你把它玩透了,后面学CSSTransition和SwitchTransition就会轻松很多。我个人建议你动手写几个小例子,比如弹窗、提示条、菜单展开收起,把四个状态都打印出来看看。嗯,实践出真知。