• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

网页中经常会见到一些动画,动画可以让产品的交互体验更好。

一般的动画我们会用 css 的 animation 和 transition 来做,但当涉及到多个元素的时候,事情就会变得复杂。

比如下面这个动画:

横线和竖线依次做动画,最后是笑脸的动画。

这么多个元素的动画如何来安排顺序呢?

如果用 css 动画来做,那要依次设置不同的动画开始时间,就很麻烦。

这种就需要用到专门的动画库了,比如 react-spring。

我们创建个 react 项目:

npx create-react-app --template=typescript react-spring-test

安装 react-spring 的包:

npm install --save @react-spring/web

然后改下 App.tsx

import { useSpringValue, animated, useSpring } from "@react-spring/web";
import { useEffect } from "react";
import "./App.css";

export default function App() {
    const width = useSpringValue(0, {
        config: {
            duration: 2000,
        },
    });

    useEffect(() => {
        width.start(300);
    }, []);

    return <animated.div className="box" style={{ width }}></animated.div>;
}

还有 App.css

.box {
    background: blue;
    height: 100px;
}

跑一下开发服务:

npm run start

可以看到,box 会在 2s 内完成 width 从 0 到 300 的动画:

此外,你还可以不定义 duration,而是定义摩擦力等参数:

const width = useSpringValue(0, {
    config: {
        // duration: 2000
        mass: 2,
        friction: 10,
        tension: 200,
    },
});

先看效果:

是不是像弹簧一样?

弹簧的英文是 spring,这也是为什么这个库叫做 react-spring

以及为什么 logo 是这样的:

它主打的就是这种弹簧动画。

当然,你不想做这种动画,直接指定 duration 也行,那就是常规的动画了。

回过头来看下这三个参数:

  • mass: 质量(也就是重量),质量越大,回弹惯性越大,回弹的距离和次数越多
  • tension: 张力,弹簧松紧程度,弹簧越紧,回弹速度越快
  • friction:摩擦力,增加点阻力可以抵消质量和张力的效果

这些参数设置不同的值,弹簧动画的效果就不一样:

tension: 400

tension: 100

可以看到,确实 tension(弹簧张力)越大,弹簧越紧,回弹速度越快。

mass: 2

mass: 20

可以看到,mass(质量越大),惯性越大,回弹距离和次数越大。

friction: 10

friction: 80

可以看到,firction(摩擦力)越大,tension 和 mass 的效果抵消的越多。

这就是弹簧动画的 3 个参数。

回过头来,我们继续看其它的 api。

如果有多个 style 都要变化呢?

这时候就不要用 useSpringValue 了,而是用 useSpring:

import { useSpring, animated } from "@react-spring/web";
import "./App.css";

export default function App() {
    const styles = useSpring({
        from: {
            width: 0,
            height: 0,
        },
        to: {
            width: 200,
            height: 200,
        },
        config: {
            duration: 2000,
        },
    });

    return <animated.div className="box" style={{ ...styles }}></animated.div>;
}

用 useSpring 指定 from 和 to,并指定 duration。

动画效果如下:

当然,也可以不用 duration 的方式:

而是用弹簧动画的效果:

useSpring 还有另外一种传入函数的重载,这种重载会返回 [styles, api] 两个参数:

import { useSpring, animated } from "@react-spring/web";
import "./App.css";

export default function App() {
    const [styles, api] = useSpring(() => {
        return {
            from: {
                width: 100,
                height: 100,
            },
            config: {
                // duration: 2000
                mass: 2,
                friction: 10,
                tension: 400,
            },
        };
    });

    function clickHandler() {
        api.start({
            width: 200,
            height: 200,
        });
    }

    return (
        <animated.div
            className="box"
            style={{ ...styles }}
            onClick={clickHandler}></animated.div>
    );
}

可以用返回的 api 来控制动画的开始。

那如果有多个元素都要同时做动画呢?

这时候就用 useSprings:

import { useSprings, animated } from "@react-spring/web";
import "./App.css";

export default function App() {
    const [springs, api] = useSprings(3, () => ({
        from: { width: 0 },
        to: { width: 300 },
        config: {
            duration: 1000,
        },
    }));

    return (
        <div>
            {springs.map((styles) => (
                <animated.div style={styles} className="box"></animated.div>
            ))}
        </div>
    );
}

在 css 里加一下 margin:

.box {
    background: blue;
    height: 100px;
    margin: 10px;
}

渲染出来是这样的:

当你指定了 to,那会立刻执行动画,或者不指定 to,用 api.start 来开始动画:

import { useSprings, animated } from "@react-spring/web";
import "./App.css";
import { useEffect } from "react";

export default function App() {
    const [springs, api] = useSprings(3, () => ({
        from: { width: 0 },
        config: {
            duration: 1000,
        },
    }));

    useEffect(() => {
        api.start({ width: 300 });
    }, []);

    return (
        <div>
            {springs.map((styles) => (
                <animated.div style={styles} className="box"></animated.div>
            ))}
        </div>
    );
}

那如果多个元素的动画是依次进行的呢?

这时候要用 useTrail

import { animated, useTrail } from "@react-spring/web";
import "./App.css";
import { useEffect } from "react";

export default function App() {
    const [springs, api] = useTrail(3, () => ({
        from: { width: 0 },
        config: {
            duration: 1000,
        },
    }));

    useEffect(() => {
        api.start({ width: 300 });
    }, []);

    return (
        <div>
            {springs.map((styles) => (
                <animated.div style={styles} className="box"></animated.div>
            ))}
        </div>
    );
}

用起来很简单,直接把 useSprings 换成 useTrail 就行:

可以看到,动画会依次执行,而不是同时。

接下来我们实现下文章开头的这个动画效果:

横线和竖线的动画就是用 useTrail 实现的。

而中间的笑脸使用 useSprings 同时做动画。

那多个动画如何安排顺序的呢?

用 useChain:

import {
    animated,
    useChain,
    useSpring,
    useSpringRef,
    useSprings,
    useTrail,
} from "@react-spring/web";
import "./App.css";

export default function App() {
    const api1 = useSpringRef();

    const [springs] = useTrail(
        3,
        () => ({
            ref: api1,
            from: { width: 0 },
            to: { width: 300 },
            config: {
                duration: 1000,
            },
        }),
        []
    );

    const api2 = useSpringRef();

    const [springs2] = useSprings(
        3,
        () => ({
            ref: api2,
            from: { height: 100 },
            to: { height: 50 },
            config: {
                duration: 1000,
            },
        }),
        []
    );

    useChain([api1, api2], [0, 1], 500);

    return (
        <div>
            {springs.map((styles1, index) => (
                <animated.div
                    style={{ ...styles1, ...springs2[index] }}
                    className="box"></animated.div>
            ))}
        </div>
    );
}

我们用 useSpringRef 拿到两个动画的 api,然后用 useChain 来安排两个动画的顺序。

useChain 的第二个参数指定了 0 和 1,第三个参数指定了 500,那就是第一个动画在 0s 开始,第二个动画在 500ms 开始。

如果第三个参数指定了 3000,那就是第一个动画在 0s 开始,第二个动画在 3s 开始。

可以看到,确实第一个动画先执行,然后 0.5s 后第二个动画执行。

这样,就可以实现那个笑脸动画了。

我们来写一下:

import {
    useTrail,
    useChain,
    useSprings,
    animated,
    useSpringRef,
} from "@react-spring/web";
import "./styles.css";
import { useEffect } from "react";

const STROKE_WIDTH = 0.5;

const MAX_WIDTH = 150;
const MAX_HEIGHT = 100;

export default function App() {
    const gridApi = useSpringRef();

    const gridSprings = useTrail(16, {
        ref: gridApi,
        from: {
            x2: 0,
            y2: 0,
        },
        to: {
            x2: MAX_WIDTH,
            y2: MAX_HEIGHT,
        },
    });

    useEffect(() => {
        gridApi.start();
    });

    return (
        <div className="container">
            <svg viewBox={`0 0 ${MAX_WIDTH} ${MAX_HEIGHT}`}>
                <g>
                    {gridSprings.map(({ x2 }, index) => (
                        <animated.line
                            x1={0}
                            y1={index * 10}
                            x2={x2}
                            y2={index * 10}
                            key={index}
                            strokeWidth={STROKE_WIDTH}
                            stroke="currentColor"
                        />
                    ))}
                    {gridSprings.map(({ y2 }, index) => (
                        <animated.line
                            x1={index * 10}
                            y1={0}
                            x2={index * 10}
                            y2={y2}
                            key={index}
                            strokeWidth={STROKE_WIDTH}
                            stroke="currentColor"
                        />
                    ))}
                </g>
            </svg>
        </div>
    );
}

当用 useSpringRef 拿到动画引用时,需要手动调用 start 来开始动画。

用 useTrail 来做从 0 到指定 width、height 的动画。

然后分别遍历它,拿到 x、y 的值,来绘制横线和竖线。

用 svg 的 line 来画线,设置 x1、y1、x2、y2 就是一条线。

效果是这样的:

当你注释掉横线或者竖线,会更明显一点:

然后再做笑脸的动画,这个就是用 rect 在不同画几个方块,做一个 scale 从 0 到 1 的动画:

动画用弹簧动画的方式,指定 mass(质量) 和 tension(张力),并且每个 box 都有不同的 delay:

并用 useChain 来把两个动画串联执行。

import {
    useTrail,
    useChain,
    useSprings,
    animated,
    useSpringRef,
} from "@react-spring/web";
import "./styles.css";

const COORDS = [
    [50, 30],
    [90, 30],
    [50, 50],
    [60, 60],
    [70, 60],
    [80, 60],
    [90, 50],
];

const STROKE_WIDTH = 0.5;

const MAX_WIDTH = 150;
const MAX_HEIGHT = 100;

export default function App() {
    const gridApi = useSpringRef();

    const gridSprings = useTrail(16, {
        ref: gridApi,
        from: {
            x2: 0,
            y2: 0,
        },
        to: {
            x2: MAX_WIDTH,
            y2: MAX_HEIGHT,
        },
    });

    const boxApi = useSpringRef();

    const [boxSprings] = useSprings(7, (i) => ({
        ref: boxApi,
        from: {
            scale: 0,
        },
        to: {
            scale: 1,
        },
        delay: i * 200,
        config: {
            mass: 2,
            tension: 220,
        },
    }));

    useChain([gridApi, boxApi], [0, 1], 1500);

    return (
        <div className="container">
            <svg viewBox={`0 0 ${MAX_WIDTH} ${MAX_HEIGHT}`}>
                <g>
                    {gridSprings.map(({ x2 }, index) => (
                        <animated.line
                            x1={0}
                            y1={index * 10}
                            x2={x2}
                            y2={index * 10}
                            key={index}
                            strokeWidth={STROKE_WIDTH}
                            stroke="currentColor"
                        />
                    ))}
                    {gridSprings.map(({ y2 }, index) => (
                        <animated.line
                            x1={index * 10}
                            y1={0}
                            x2={index * 10}
                            y2={y2}
                            key={index}
                            strokeWidth={STROKE_WIDTH}
                            stroke="currentColor"
                        />
                    ))}
                </g>
                {boxSprings.map(({ scale }, index) => (
                    <animated.rect
                        key={index}
                        width={10}
                        height={10}
                        fill="currentColor"
                        style={{
                            transform: `translate(${COORDS[index][0]}px, ${COORDS[index][1]}px)`,
                            transformOrigin: `5px 5px`,
                            scale,
                        }}
                    />
                ))}
            </svg>
        </div>
    );
}

这样,整个动画就完成了:

这个动画,我们综合运用了 useSprings、useTrail、useSpringRef、useChain 这些 api。

把这个动画写一遍,react-spring 就算是掌握的可以了。

案例代码上传了小册仓库

其实这是 react-spring 的 官方案例 里的一个,基础 api 会了之后,大家可以把这些案例都过一遍。

总结

我们学了用 react-spring 来做动画。

react-spring 主打的是弹簧动画,就是类似弹簧那种回弹效果。

只要指定 mass(质量)、tension(张力)、friction(摩擦力)就可以了。

  • mass 质量:决定回弹惯性,mass 越大,回弹的距离和次数越多。

  • tension 张力:弹簧松紧程度,弹簧越紧,回弹速度越快。

  • friction:摩擦力: 可以抵消质量和张力的效果

弹簧动画不需要指定时间。

当然,你也可以指定 duration 来做那种普通动画。

react-spring 有不少 api,分别用于单个、多个元素的动画:

  • useSpringValue:指定单个属性的变化。
  • useSpring:指定多个属性的变化
  • useSprings:指定多个元素的多个属性的变化,动画并行执行
  • useTrial:指定多个元素的多个属性的变化,动画依次执行
  • useSpringRef:用来拿到每个动画的 ref,可以用来控制动画的开始、暂停等
  • useChain:串行执行多个动画,每个动画可以指定不同的开始时间

掌握了这些,就足够基于 react-spring 做动画了。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
22.自定义hook练习(二)
Next
24.react-spring结合use-gesture手势库实现交互动画