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搞不定的场景。