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>
  );
}

嗯,这里要注意——enteringentered的样式可以一样,但它们的语义完全不同。前者是过渡中,后者是已稳定。我在项目中见过有人把动画逻辑全塞在entered里,结果动画根本不执行。为什么?因为entered是最终态,CSS transition需要从enteringentered的变化过程才能触发。

4.2 自定义动画时长与延迟

Transition组件默认用timeout控制动画时长。但实际项目中,你往往需要更精细的控制。

timeout可以传一个数字,也可以传一个对象:

<Transition
  in={inProp}
  timeout={{
    enter: 500,
    exit: 300,
  }}
>
  {state => (/* ... */)}
</Transition>

这样进入动画500ms,退出动画300ms。我个人习惯把进入动画做得比退出长一点,因为用户更关注内容出现的过程,退出时快一点反而感觉干脆。

那延迟呢?比如我想让元素在进入前先等200ms再开始动画。可以用onEntering回调配合setTimeout,但更优雅的方式是用CSS的animation-delaytransition-delay

const defaultStyle = {
  transition: `opacity 500ms ease-in-out 200ms`, // 200ms延迟
  opacity: 0,
};

小技巧:如果你需要多个元素依次出现(比如列表动画),可以给每个元素递增的transition-delay。我曾经用这个技巧做了一个卡片瀑布流效果,用户体验非常好。

4.3 回调函数:精准控制动画节点

Transition组件提供了四个回调:onEnteronEnteringonEnteredonExitonExitingonExited。它们分别在状态切换时触发。

举个例子,我想在动画结束后执行一些清理工作:

<Transition
  in={inProp}
  timeout={300}
  onEntered={() => console.log('进入完成')}
  onExited={() => console.log('退出完成,可以卸载了')}
>
  {state => (/* ... */)}
</Transition>

避坑指南:我曾经在onExited里直接调用了setState来移除DOM元素,结果造成了无限循环。因为setState导致组件重新渲染,又触发了in的变化。正确的做法是:在onExited里只做标记,由父组件控制in的值。

4.4 appear属性:首次渲染也动画

默认情况下,Transition组件只在infalse变为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就会轻松很多。我个人建议你动手写几个小例子,比如弹窗、提示条、菜单展开收起,把四个状态都打印出来看看。嗯,实践出真知。