1、React动画基础:CSS过渡与动画在React中的应用,理解组件生命周期与动画触发时机

1.1 为什么React动画要从CSS说起?

说实话,很多新手一上来就想着用第三方动画库。我个人习惯是,先问自己一句:「这个动画真的需要JS来管吗?」

CSS过渡和动画,是React动画体系的地基。你想想看,React本身只是个UI状态机,它只管「组件该长什么样」。至于怎么从A状态变到B状态,CSS才是真正的执行者。

我在项目中遇到过不少情况——用JS动画库写了半天,最后发现一个CSS transition就能搞定。嗯,这里要注意:别把简单问题复杂化。

1.2 CSS过渡:最简单的入场方式

CSS过渡(transition)适合处理「状态变化」的场景。比如鼠标悬停、显示/隐藏、尺寸变化等。

来看一个最基础的例子:

// FadeInBox.jsx
import React, { useState } from 'react';
import './FadeInBox.css';

function FadeInBox() {
  const [visible, setVisible] = useState(false);

  return (
    <div>
      <button onClick={() => setVisible(!visible)}>
        {visible ? '隐藏' : '显示'}
      </button>
      <div className={`box ${visible ? 'show' : ''}`}>
        我是动画盒子
      </div>
    </div>
  );
}
/* FadeInBox.css */
.box {
  width: 200px;
  height: 200px;
  background: #4A90D9;
  opacity: 0;
  transform: translateY(20px);
  transition: all 0.4s ease;
}

.box.show {
  opacity: 1;
  transform: translateY(0);
}

这里的关键点是什么?CSS transition 监听的是 className 的变化。React 通过 setState 改变了 visible,然后 className 变了,CSS 自动触发过渡。

核心原理:React 负责「状态 → className」的映射,CSS 负责「className → 视觉变化」的动画执行。各司其职。

1.3 组件生命周期与动画触发时机

为什么会强调生命周期?因为动画本质上是在「组件状态切换的瞬间」发生的。

React 组件有三个关键阶段:

阶段 触发时机 动画场景
挂载(Mount) 组件首次渲染到DOM 入场动画(淡入、滑入)
更新(Update) props或state变化 状态切换动画(展开/收起)
卸载(Unmount) 组件从DOM移除 出场动画(淡出、滑出)

这里有个坑——CSS transition 无法直接处理「卸载」动画。为什么?因为组件一卸载,DOM 节点就没了,CSS 根本没机会执行动画。

避坑指南:我曾经在项目里直接用 CSS transition 做列表删除动画,结果发现元素瞬间消失。后来才意识到,卸载动画需要配合 React 的「延迟卸载」技巧——先让组件进入「隐藏状态」,等动画执行完再真正移除。

1.4 CSS动画:更复杂的序列控制

CSS 动画(animation)比 transition 更强大。它可以定义多帧、循环、延迟等复杂行为。

举个例子,做一个「弹跳入场」效果:

/* BounceIn.css */
@keyframes bounceIn {
  0% {
    opacity: 0;
    transform: scale(0.3);
  }
  50% {
    opacity: 1;
    transform: scale(1.1);
  }
  70% {
    transform: scale(0.9);
  }
  100% {
    transform: scale(1);
  }
}

.bounce-box {
  animation: bounceIn 0.6s ease-out;
}
// BounceIn.jsx
function BounceIn() {
  const [show, setShow] = useState(false);

  return (
    <div>
      <button onClick={() => setShow(!show)}>
        触发弹跳
      </button>
      {show && <div className="bounce-box">我弹进来了!</div>}
    </div>
  );
}

注意看,这里我用的是 show && <div> 这种条件渲染。每次 show 变成 true,组件重新挂载,CSS animation 就会重新播放。

小技巧:如果你想让动画只播放一次,用 animation-fill-mode: forwards 保持结束状态。如果想重复播放,用 animation-iteration-count: infinite。我个人习惯把动画参数抽成 CSS 变量,方便在 JS 里动态控制。

1.5 实战:用CSS过渡实现手风琴组件

光说不练假把式。咱们写一个常见的手风琴(Accordion)组件,看看生命周期和动画怎么配合。

// Accordion.jsx
import React, { useState, useRef } from 'react';
import './Accordion.css';

function Accordion({ title, children }) {
  const [isOpen, setIsOpen] = useState(false);
  const contentRef = useRef(null);

  return (
    <div className="accordion">
      <button
        className="accordion-header"
        onClick={() => setIsOpen(!isOpen)}
      >
        {title}
        <span className={`arrow ${isOpen ? 'open' : ''}`}>▸</span>
      </button>
      <div
        className="accordion-content"
        style={{
          maxHeight: isOpen ? contentRef.current?.scrollHeight : 0,
          opacity: isOpen ? 1 : 0
        }}
      >
        <div ref={contentRef}>
          {children}
        </div>
      </div>
    </div>
  );
}
/* Accordion.css */
.accordion-content {
  overflow: hidden;
  transition: max-height 0.3s ease, opacity 0.3s ease;
}

.accordion-header {
  cursor: pointer;
  padding: 12px 16px;
  background: #f0f4ff;
  border: 1px solid #d0d8e8;
  width: 100%;
  text-align: left;
}

.arrow {
  transition: transform 0.3s ease;
  display: inline-block;
}

.arrow.open {
  transform: rotate(90deg);
}

这里有个细节:maxHeight 动画。直接用 height 做动画不行,因为内容高度是动态的。用 maxHeight 配合 scrollHeight,就能实现平滑展开。

经验之谈:我在做企业后台系统时,大量使用了这种「maxHeight + opacity」的组合。它兼容性好,性能也不错。唯一要注意的是 maxHeight 的初始值要设得足够大,否则内容多了会截断。

1.6 性能:别让动画拖垮你的页面

动画做得好,用户体验就上去了。但做得不好,页面卡成PPT。

CSS 动画的性能关键点:

  • 优先使用 transform 和 opacity——这两个属性不会触发重排(reflow),只触发合成(composite)
  • 避免 animating width/height/top/left——这些会触发布局计算,性能开销大
  • 使用 will-change 提示浏览器——比如 will-change: transform, opacity

举个例子,同样是移动一个盒子:

/* ❌ 性能差 */
.box-bad {
  left: 0;
  transition: left 0.3s;
}
.box-bad.move {
  left: 100px;
}

/* ✅ 性能好 */
.box-good {
  transform: translateX(0);
  transition: transform 0.3s;
}
.box-good.move {
  transform: translateX(100px);
}

你想想看,left 变化会让浏览器重新计算整个布局。而 transform 只是把盒子「画」到新位置,GPU 直接处理,快得多。

注意:will-change 别滥用。我曾经在一个项目里给所有动画元素都加了 will-change,结果内存占用飙升。只在真正需要动画的元素上加,用完记得移除。

1.7 小结:什么时候用CSS,什么时候用JS?

说了这么多,最后给个判断标准:

场景 推荐方案 原因
简单的状态切换(显示/隐藏、悬停) CSS transition 代码少、性能好、浏览器原生优化
复杂的多帧动画(弹跳、旋转) CSS animation @keyframes 表达能力足够
需要精确控制时间线(暂停、反向) JS动画库(如Framer Motion) CSS 无法动态控制播放进度
卸载动画(组件消失) JS + CSS 配合 CSS 无法感知组件卸载时机

说白了,能用CSS解决的,就别上JS。CSS动画跑在合成线程上,不阻塞主线程。JS动画再优化,也绕不开事件循环的开销。

下一章,咱们会深入React的「条件渲染」和「列表动画」,看看怎么用React Transition Group这类工具,优雅地处理那些CSS搞不定的场景。