普通视图

发现新文章,点击刷新页面。
昨天 — 2025年10月29日首页

react-konva实战指南:Canvas高性能+易维护的组件化图形开发实现教程

作者 鹏多多
2025年10月28日 08:38

图形绘制与交互是许多复杂应用(如数据可视化、设计工具、画板,游戏等)的核心需求。而react-konva作为Konva.js的React封装库,将React的声明式编程理念与Konva.js强大的图形处理能力完美结合,让开发者能够以更直观、高效的方式构建交互式图形应用。我将从react-konva的核心特性出发,详细讲解其使用方法、性能优化技巧及实际应用场景,帮助读者快速上手并落地项目。

1. 介绍

react-konva并非一个独立的图形库,而是Konva.js与React的桥梁。Konva.js是一款基于Canvas的2D图形库,支持分层渲染、事件检测、动画过渡等核心能力,而react-konva则通过React组件的形式封装了Konva.js的API,让开发者可以用React的思维(如组件化、状态管理、Props传递)来操作图形元素,无需直接编写原生Canvas代码。

核心优势

  1. 声明式API:通过React组件(如<Stage><Layer><Rect><Circle>)描述图形结构,替代Konva.js的命令式调用,代码更易读、维护;
  2. React生态兼容:无缝集成React的状态管理(如useStateuseReducer)、生命周期(如useEffect),支持Redux、MobX等状态库;
  3. 高性能渲染:基于Konva.js的分层渲染机制,仅更新变化的图形元素,避免全量重绘;
  4. 完善的事件系统:支持鼠标(onClickonDrag)、触摸(onTouchStart)、键盘(onKeyPress)等事件,且事件检测精度不受Canvas像素限制;
  5. 丰富的图形与动画:内置矩形、圆形、文本、路径等基础图形,支持缩放、旋转、平移等变换,以及帧动画、过渡动画。

2.快速上手

从安装到第一个图形,步骤如下:

2.1. 安装依赖

react-konva依赖于konva核心库,需同时安装两个包:

# npm
npm install react-konva konva --save

# yarn
yarn add react-konva konva

2.2. 基础示例

react-konva的核心组件结构为:Stage(画布容器)→ Layer(渲染层)→ 图形元素(RectCircle等)。其中,Stage是顶层容器,一个应用可包含多个StageLayer是渲染层,每个Layer对应一个Canvas元素,建议将“频繁更新的元素”与“静态元素”分属不同Layer以优化性能。

以下是一个完整的示例,实现“点击按钮添加一个可拖拽的矩形”的功能:

import React, { useState } from 'react';
import { Stage, Layer, Rect, Text } from 'react-konva';

const App = () => {
  // 状态:存储所有矩形的信息(位置、大小、颜色)
  const [rectangles, setRectangles] = useState([
    { x: 50, y: 50, width: 100, height: 60, color: '#ff6347' }
  ]);
  // 状态:记录当前是否在拖拽矩形
  const [isDragging, setIsDragging] = useState(false);

  // 新增矩形:在随机位置添加一个蓝色矩形
  const addRectangle = () => {
    setRectangles([
      ...rectangles,
      {
        x: Math.random() * 400, // 随机X坐标(Stage宽度为500)
        y: Math.random() * 300, // 随机Y坐标(Stage高度为400)
        width: 80 + Math.random() * 60, // 随机宽度
        height: 50 + Math.random() * 40, // 随机高度
        color: '#4169e1'
      }
    ]);
  };

  // 拖拽事件:开始拖拽时更新状态
  const handleDragStart = () => {
    setIsDragging(true);
  };

  // 拖拽事件:结束拖拽时更新状态
  const handleDragEnd = (e) => {
    setIsDragging(false);
    // 更新被拖拽矩形的最终位置
    const updatedRects = rectangles.map((rect, index) => {
      if (index === e.target.index) { // e.target.index 是当前图形在父组件中的索引
        return { ...rect, x: e.target.x(), y: e.target.y() };
      }
      return rect;
    });
    setRectangles(updatedRects);
  };

  return (
    <div style={{ margin: '20px' }}>
      {/* 按钮:触发新增矩形 */}
      <button 
        onClick={addRectangle}
        style={{ marginBottom: '10px', padding: '8px 16px' }}
      >
        添加矩形
      </button>
      {/* 拖拽状态提示 */}
      {isDragging && <Text text="拖拽中..." x={200} y={10} fontSize={16} />}

      {/* Stage:画布容器,width/height 定义画布大小 */}
      <Stage width={500} height={400} style={{ border: '1px solid #eee' }}>
        {/* Layer:渲染层,所有图形元素必须放在Layer内 */}
        <Layer>
          {/* 遍历渲染所有矩形 */}
          {rectangles.map((rect, index) => (
            <Rect
              key={index} // 建议使用唯一ID此处为简化用index
              x={rect.x}
              y={rect.y}
              width={rect.width}
              height={rect.height}
              fill={rect.color}
              stroke="#333" // 边框颜色
              strokeWidth={2} // 边框宽度
              draggable // 允许拖拽
              onDragStart={handleDragStart}
              onDragEnd={handleDragEnd}
              // 鼠标悬停时显示指针
              onMouseOver={(e) => {
                e.target.setAttrs({ stroke: '#ff0' }); // 悬停时边框变黄
              }}
              onMouseOut={(e) => {
                e.target.setAttrs({ stroke: '#333' }); // 离开时恢复边框颜色
              }}
            />
          ))}
        </Layer>
      </Stage>
    </div>
  );
};

export default App;

2.3. 核心组件解析

组件 作用说明
<Stage> 顶层画布容器,对应Konva.js的Konva.Stage,需指定widthheight属性
<Layer> 渲染层,对应Konva.Layer,每个Layer包含一个Canvas元素,支持分层渲染
<Rect> 矩形图形,支持x(横坐标)、y(纵坐标)、widthheightfill(填充色)等属性
<Circle> 圆形图形,核心属性为xy(圆心坐标)、radius(半径)、fill
<Text> 文本元素,支持text(内容)、fontSizefontFamilyfill等属性
<Image> 图片元素,需通过image属性传入Image对象(需先加载完成)

3. 进阶功能

下面是一些进阶的功能,包括动画、变换与事件:

3.1. 实现图形动画

react-konva支持两种动画方式:基于状态的动画(通过React状态更新触发重绘)和Konva原生动画(通过Konva.Animation API)。

方式1:基于状态的简单动画(基础)

通过useState+useEffect实现矩形的“呼吸效果”(缩放动画),适合基础过渡:

import React, { useState, useEffect } from 'react';
import { Stage, Layer, Rect } from 'react-konva';

const AnimatedRect = () => {
  const [scale, setScale] = useState(1); // 缩放比例,初始为1
  const [growing, setGrowing] = useState(true); // 是否正在放大

  // 每30ms更新一次缩放比例,实现动画效果
  useEffect(() => {
    const timer = setInterval(() => {
      setScale(prev => {
        // 放大到1.2后开始缩小,缩小到0.8后开始放大
        if (prev >= 1.2) setGrowing(false);
        if (prev <= 0.8) setGrowing(true);
        return growing ? prev + 0.01 : prev - 0.01;
      });
    }, 30);

    // 组件卸载时清除定时器,避免内存泄漏
    return () => clearInterval(timer);
  }, [growing]);

  return (
    <Stage width={300} height={200}>
      <Layer>
        <Rect
          x={100}
          y={50}
          width={100}
          height={60}
          fill="#20b2aa"
          scaleX={scale} // X轴缩放比例
          scaleY={scale} // Y轴缩放比例
          offsetX={50} // 缩放中心点X矩形宽度的一半offsetY={30} // 缩放中心点Y矩形高度的一半)
        />
      </Layer>
    </Stage>
  );
};

export default AnimatedRect;

方式2:Konva原生动画(复杂)

对于需要精细控制的复杂帧动画(如多属性同步变化、物理运动),建议使用Konva的Animate组件或Konva.Animation API:

import React from 'react';
import { Stage, Layer, Rect, Animate } from 'react-konva';

const ComplexAnimation = () => {
  // 定义动画关键帧:x从50→400,y从50→250,同时旋转360度
  const animationConfig = {
    x: [50, 400],
    y: [50, 250],
    rotation: [0, 360], // 旋转角度(单位:度)
    duration: 2000, // 动画时长(ms)
    easing: Konva.Easings.EaseInOut // 缓动函数
  };

  return (
    <Stage width={500} height={300}>
      <Layer>
        <Rect
          width={80}
          height={50}
          fill="#ff4500"
          offsetX={40} // 旋转中心点矩形中心offsetY={25}
        >
          {/* Animate组件:绑定动画配置 */}
          <Animate
            config={animationConfig}
            repeat={Infinity} // 无限循环
            yoyo={true} // 动画结束后反向播放类似往返效果)
          />
        </Rect>
      </Layer>
    </Stage>
  );
};

export default ComplexAnimation;

3.2. 图形变换(缩放、旋转、平移)

react-konva的图形元素支持通过属性直接控制变换,核心属性包括:

  • x/y:元素的左上角坐标(默认基准点为左上角);
  • scaleX/scaleY:X/Y轴缩放比例(1为原始大小);
  • rotation:旋转角度(单位:度,顺时针为正);
  • offsetX/offsetY:变换基准点(如设置为元素中心,旋转/缩放将围绕中心进行)。

示例:通过滑块控制矩形的旋转角度:

import React, { useState } from 'react';
import { Stage, Layer, Rect } from 'react-konva';

const RotatableRect = () => {
  const [rotation, setRotation] = useState(0); // 初始旋转角度为0

  return (
    <div style={{ margin: '20px' }}>
      {/* 滑块:控制旋转角度(0~360度) */}
      <label>旋转角度:{rotation}°</label>
      <input
        type="range"
        min="0"
        max="360"
        value={rotation}
        onChange={(e) => setRotation(Number(e.target.value))}
        style={{ width: '300px', marginLeft: '10px' }}
      />

      <Stage width={300} height={200}>
        <Layer>
          <Rect
            x={150}
            y={100}
            width={100}
            height={60}
            fill="#9370db"
            rotation={rotation}
            offsetX={50} // 旋转基准点为矩形中心
            offsetY={30}
            stroke="#333"
            strokeWidth={2}
          />
        </Layer>
      </Stage>
    </div>
  );
};

export default RotatableRect;

3.3. 事件类型和处理

react-konva的事件系统基于Konva.js,能精准捕获与交互,支持像素级别的事件检测(即使两个图形重叠,也能精准识别鼠标 hover 的是哪个图形),且事件名称与React保持一致(如onClickonMouseMove)。

常见事件类型:

  • 鼠标事件:onClickonDoubleClickonMouseDownonMouseUponMouseOveronMouseOut
  • 拖拽事件:onDragStartonDragonDragEnd
  • 触摸事件:onTouchStartonTouchMoveonTouchEnd
  • 键盘事件:需先通过stage.on('keydown', handler)绑定,或在元素上使用onKeyPress(需元素处于焦点状态)。

示例:实现“点击矩形改变颜色”和“键盘删除选中矩形”:

import React, { useState, useRef } from 'react';
import { Stage, Layer, Rect } from 'react-konva';

const InteractiveRects = () => {
  const [rectangles, setRectangles] = useState([
    { id: 1, x: 50, y: 50, width: 80, height: 50, color: '#ff6b6b' },
    { id: 2, x: 200, y: 100, width: 80, height: 50, color: '#4ecdc4' }
  ]);
  const [selectedId, setSelectedId] = useState(null);
  const stageRef = useRef(null); // 用于获取Stage实例

  // 点击矩形:选中并改变颜色
  const handleRectClick = (e, id) => {
    setSelectedId(id);
    // 随机改变颜色
    const randomColor = `#${Math.floor(Math.random() * 16777215).toString(16)}`;
    const updatedRects = rectangles.map(rect => 
      rect.id === id ? { ...rect, color: randomColor } : rect
    );
    setRectangles(updatedRects);
  };

  // 键盘事件:按Delete删除选中的矩形
  React.useEffect(() => {
    const handleKeyDown = (e) => {
      if (e.key === 'Delete' && selectedId) {
        setRectangles(rectangles.filter(rect => rect.id !== selectedId));
        setSelectedId(null);
      }
    };

    // 绑定键盘事件
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [selectedId, rectangles]);

  return (
    <div>
      <p>点击矩形选中并改变颜色,按Delete删除选中矩形</p>
      <Stage 
        width={400} 
        height={200} 
        ref={stageRef}
        style={{ border: '1px solid #eee' }}
      >
        <Layer>
          {rectangles.map(rect => (
            <Rect
              key={rect.id}
              x={rect.x}
              y={rect.y}
              width={rect.width}
              height={rect.height}
              fill={rect.color}
              stroke={selectedId === rect.id ? '#ff0' : '#333'} // 选中时边框变黄
              strokeWidth={selectedId === rect.id ? 3 : 2} // 选中时边框变粗
              onClick={(e) => handleRectClick(e, rect.id)}
              onMouseOut={(e) => {
                e.target.setAttrs({
                  stroke: selectedId === rect.id ? '#ff0' : '#333',
                });
              }}
            />
          ))}
        </Layer>
      </Stage>
    </div>
  );
};

export default InteractiveRects;

4. 性能优化

应对大规模图形场景,当应用中需要渲染成百上千个图形元素(如数据可视化中的海量节点、设计工具中的复杂图层)时,单纯的基础用法可能会出现卡顿。react-konva虽基于 Konva.js 做了底层优化,但仍需结合 React 特性进行针对性优化,核心思路是减少不必要的重渲染降低绘制压力

4.1. 避免不必要的组件重渲染

React 组件的重渲染触发条件(如父组件重渲染、Props 变化、State 变化)会直接影响react-konva的性能,可通过以下方式优化:

方式1:使用 memo 缓存图形组件

对于纯展示型的图形组件(如静态矩形、文本),可通过 React.memo 缓存组件,避免父组件重渲染时被连带重渲染。

示例:封装一个缓存的矩形组件:

import React, { memo } from 'react';
import { Rect } from 'react-konva';

// 自定义比较函数:仅当Props中的关键属性变化时才重渲染
const RectMemoized = memo(
  ({ x, y, width, height, color, onMouseOver, onMouseOut }) => (
    <Rect
      x={x}
      y={y}
      width={width}
      height={height}
      fill={color}
      stroke="#333"
      strokeWidth={2}
      onMouseOver={onMouseOver}
      onMouseOut={onMouseOut}
    />
  ),
  (prevProps, nextProps) => {
    // 仅当关键属性(位置、大小、颜色)不变时,返回true(不重渲染)
    return (
      prevProps.x === nextProps.x &&
      prevProps.y === nextProps.y &&
      prevProps.width === nextProps.width &&
      prevProps.height === nextProps.height &&
      prevProps.color === nextProps.color
    );
  }
);

export default RectMemoized;

方式2:拆分状态与分层渲染

将“频繁变化的元素”(如拖拽中的图形、实时更新的数据标签)与“静态元素”(如背景、固定参考线)拆分到不同的 <Layer> 中。Konva.js 会仅重绘变化的 Layer,而非全量重绘整个 Stage。

示例:分层管理静态背景与动态图形:

<Stage width={800} height={600}>
  {/* 静态Layer:仅渲染一次,后续不重绘 */}
  <Layer>
    <Rect x={0} y={0} width={800} height={600} fill="#f5f5f5" /> {/* 背景 */}
    <Line points={[0, 300, 800, 300]} stroke="#ddd" strokeWidth={1} /> {/* 参考线 */}
  </Layer>

  {/* 动态Layer:仅当图形变化时重绘 */}
  <Layer>
    {dynamicRectangles.map(rect => (
      <RectMemoized key={rect.id} {...rect} />
    ))}
  </Layer>
</Stage>

方式3:使用 useCallback 缓存事件处理函数

若图形组件的事件处理函数(如 onClickonDrag)是在父组件中定义的,每次父组件重渲染时会生成新的函数实例,导致子组件 Props 变化而重渲染。可通过 useCallback 缓存函数。

示例:缓存拖拽事件处理函数:

const handleDragEnd = useCallback((e, id) => {
  setRectangles(prev => 
    prev.map(rect => 
      rect.id === id ? { ...rect, x: e.target.x(), y: e.target.y() } : rect
    )
  );
}, []); // 依赖为空,函数仅创建一次

4.2. 降低绘制压力

当图形数量超过 1000 个时,即使避免了重渲染,Canvas 的绘制操作仍可能成为瓶颈,可通过以下方式优化:

方式1:图形合并

对于大量重复且无交互的图形(如数据可视化中的网格点、背景纹理),可通过 Konva.js 的 Group 组件合并,批量绘制,减少绘制调用次数。

示例:合并多个静态小圆点:

import { Group, Circle } from 'react-konva';

const DotGroup = () => {
  // 生成1000个静态小圆点
  const dots = Array.from({ length: 1000 }, (_, i) => ({
    id: i,
    x: Math.random() * 800,
    y: Math.random() * 600,
    radius: 2,
    color: '#ccc'
  }));

  return (
    <Group> {/* 合并为一个Group,减少绘制调用 */}
      {dots.map(dot => (
        <Circle
          key={dot.id}
          x={dot.x}
          y={dot.y}
          radius={dot.radius}
          fill={dot.color}
        />
      ))}
    </Group>
  );
};

方式2:可视区域裁剪

仅渲染当前视图内的图形(Viewport Culling),隐藏视图外的图形(如滚动或缩放时)。可通过监听 Stage 的 zoomdrag 事件,计算可视区域范围,过滤掉不在范围内的图形。

示例:实现可视区域裁剪:

import React, { useState, useEffect } from 'react';
import { Stage, Layer, RectMemoized } from 'react-konva';

const ViewportCulling = () => {
  const [allRectangles, setAllRectangles] = useState([]);
  const [visibleRectangles, setVisibleRectangles] = useState([]);
  const stageRef = useRef(null);

  // 初始化10000个矩形(模拟大规模数据)
  useEffect(() => {
    const rects = Array.from({ length: 10000 }, (_, i) => ({
      id: i,
      x: Math.random() * 2000,
      y: Math.random() * 1500,
      width: 20,
      height: 20,
      color: `#${Math.floor(Math.random() * 16777215).toString(16)}`
    }));
    setAllRectangles(rects);
  }, []);

  // 监听Stage的缩放和拖拽事件,更新可视区域内的图形
  useEffect(() => {
    const stage = stageRef.current;
    if (!stage) return;

    const updateVisibleRects = () => {
      // 获取Stage的可视区域范围(考虑缩放和偏移)
      const stageRect = stage.getClientRect();
      const visibleLeft = stageRect.x;
      const visibleTop = stageRect.y;
      const visibleRight = visibleLeft + stageRect.width;
      const visibleBottom = visibleTop + stageRect.height;

      // 过滤出在可视区域内的矩形
      const visible = allRectangles.filter(rect => 
        rect.x + rect.width > visibleLeft &&
        rect.x < visibleRight &&
        rect.y + rect.height > visibleTop &&
        rect.y < visibleBottom
      );

      setVisibleRectangles(visible);
    };

    // 初始计算一次
    updateVisibleRects();
    // 监听缩放和拖拽事件
    stage.on('zoom drag end', updateVisibleRects);

    // 清理事件监听
    return () => stage.off('zoom drag end', updateVisibleRects);
  }, [allRectangles]);

  return (
    <Stage
      ref={stageRef}
      width={800}
      height={600}
      draggable // 允许拖拽Stage查看大范围图形
      scaleX={1}
      scaleY={1}
      onWheel={(e) => {
        // 实现滚轮缩放
        e.evt.preventDefault();
        const scale = stageRef.current.scaleX();
        const newScale = e.evt.deltaY > 0 ? scale - 0.1 : scale + 0.1;
        stageRef.current.scale({ x: newScale, y: newScale });
      }}
    >
      <Layer>
        {visibleRectangles.map(rect => (
          <RectMemoized key={rect.id} {...rect} />
        ))}
      </Layer>
    </Stage>
  );
};

export default ViewportCulling;

5. 实际应用场景与案例

react-konva 凭借其灵活性和高性能,广泛应用于各类图形交互场景,以下是几个典型案例:

5.1. 交互式图表数据可视化

结合 d3.js 等数据处理库,可构建支持拖拽、缩放、hover 提示的交互式图表(如散点图、热力图)。

示例:基于 react-konva + d3 的散点图:

import React, { useEffect, useState } from 'react';
import { Stage, Layer, Circle, Text } from 'react-konva';
import * as d3 from 'd3';

const ScatterPlot = ({ data }) => {
  const [scaledData, setScaledData] = useState([]);
  const [hoveredPoint, setHoveredPoint] = useState(null);

  // 使用d3.scale处理数据映射(将原始数据映射到Stage坐标)
  useEffect(() => {
    const xScale = d3.scaleLinear()
      .domain([0, d3.max(data, d => d.x)])
      .range([50, 750]); // X轴范围:50~750(留出边距)

    const yScale = d3.scaleLinear()
      .domain([0, d3.max(data, d => d.y)])
      .range([550, 50]); // Y轴范围:550~50(倒序,符合视觉习惯)

    const scaled = data.map(d => ({
      id: d.id,
      x: xScale(d.x),
      y: yScale(d.y),
      value: d.value,
      color: d3.interpolateViridis(d.value / 100) // 基于value生成颜色
    }));

    setScaledData(scaled);
  }, [data]);

  return (
    <Stage width={800} height={600}>
      <Layer>
        {/* 坐标轴 */}
        <Line points={[50, 50, 50, 550]} stroke="#333" strokeWidth={2} /> {/* Y轴 */}
        <Line points={[50, 550, 750, 550]} stroke="#333" strokeWidth={2} /> {/* X轴 */}
        {/* 轴标签 */}
        <Text text="X轴(数值)" x={400} y={580} fontSize={14} align="center" />
        <Text text="Y轴(数值)" x={20} y={300} fontSize={14} rotation={-90} align="center" />

        {/* 散点 */}
        {scaledData.map(point => (
          <Circle
            key={point.id}
            x={point.x}
            y={point.y}
            radius={hoveredPoint === point.id ? 8 : 5} // hover时放大
            fill={point.color}
            stroke={hoveredPoint === point.id ? "#fff" : "none"}
            strokeWidth={2}
            onMouseOver={() => setHoveredPoint(point.id)}
            onMouseOut={() => setHoveredPoint(null)}
          />
        ))}

        {/* Hover提示框 */}
        {hoveredPoint && (
          const point = scaledData.find(d => d.id === hoveredPoint);
          <Group x={point.x + 10} y={point.y - 10}>
            <Rect width={120} height={40} fill="#fff" stroke="#333" strokeWidth={1} />
            <Text text={`Value: ${point.value}`} x={10} y={10} fontSize={12} />
            <Text text={`X: ${point.x.toFixed(0)}`} x={10} y={25} fontSize={12} />
          </Group>
        )}
      </Layer>
    </Stage>
  );
};

// 使用示例:
// <ScatterPlot data={[{ id: 1, x: 20, y: 80, value: 50 }, ...]} />
export default ScatterPlot;

5.2. 简易图形编辑器

构建支持图形添加、拖拽、旋转、删除的轻量级设计工具(如流程图编辑器、海报制作工具)。

可以实现如下核心功能:

  • 图形库:提供矩形、圆形、文本等基础图形选择;
  • 画布操作:支持画布拖拽、缩放;
  • 图层管理:显示/隐藏、锁定/解锁图层;
  • 导出功能:将画布内容导出为图片(通过 stage.toDataURL())。

5.3. 简单2D游戏开发

实现支持碰撞检测、角色动画的 2D 游戏(如贪吃蛇、拼图游戏)。

示例:贪吃蛇游戏的核心逻辑(简化):

import React, { useEffect, useRef, useState } from 'react';
import { Stage, Layer, Rect } from 'react-konva';

const SnakeGame = () => {
  const [snake, setSnake] = useState([{ x: 200, y: 200 }, { x: 190, y: 200 }, { x: 180, y: 200 }]);
  const [food, setFood] = useState({ x: 300, y: 300 });
  const [direction, setDirection] = useState({ x: 10, y: 0 }); // 初始方向:右
  const gameLoopRef = useRef(null);

  // 生成随机食物位置
  const generateFood = () => {
    const x = Math.floor(Math.random() * 40) * 10; // 10的倍数,与蛇身对齐
    const y = Math.floor(Math.random() * 30) * 10;
    setFood({ x, y });
  };

  // 游戏循环:每100ms更新一次蛇的位置
  useEffect(() => {
    gameLoopRef.current = setInterval(() => {
      setSnake(prev => {
        // 计算新蛇头位置
        const head = { x: prev[0].x + direction.x, y: prev[0].y + direction.y };
        // 检查是否吃到食物
        const ateFood = head.x === food.x && head.y === food.y;
        if (ateFood) generateFood();

        // 更新蛇身:吃到食物则增加一节,否则删除尾部
        const newSnake = [head, ...prev];
        if (!ateFood) newSnake.pop();
        return newSnake;
      });
    }, 100);

    // 清理定时器
    return () => clearInterval(gameLoopRef.current);
  }, [direction, food]);

  // 监听键盘事件控制方向
  useEffect(() => {
    const handleKeyDown = (e) => {
      switch (e.key) {
        case 'ArrowUp':
          if (direction.y !== 10) setDirection({ x: 0, y: -10 }); // 避免反向
          break;
        case 'ArrowDown':
          if (direction.y !== -10) setDirection({ x: 0, y: 10 });
          break;
        case 'ArrowLeft':
          if (direction.x !== 10) setDirection({ x: -10, y: 0 });
          break;
        case 'ArrowRight':
          if (direction.x !== -10) setDirection({ x: 10, y: 0 });
          break;
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [direction]);

  return (
    <Stage width={400} height={300}>
      <Layer>
        {/* 蛇身 */}
        {snake.map((segment, index) => (
          <Rect
            key={index}
            x={segment.x}
            y={segment.y}
            width={10}
            height={10}
            fill={index === 0 ? '#2ecc71' : '#27ae60'} // 蛇头绿色更深
          />
        ))}

        {/* 食物 */}
        <Rect
          x={food.x}
          y={food.y}
          width={10}
          height={10}
          fill="#e74c3c"
        />
      </Layer>
    </Stage>
  );
};

export default SnakeGame;

6. 常见问题与解决方案

在使用 react-konva 开发过程中,开发者常会遇到一些共性问题,以下是高频问题及对应的解决方案:

图形元素不显示

可能原因与解决方法:

  • 未放在 <Layer>:所有图形元素(RectCircle 等)必须嵌套在 <Layer> 内,否则无法渲染。
    解决方案:确保组件结构为 Stage → Layer → 图形元素

  • 坐标或尺寸设置错误:若图形的 x/y 坐标超出 <Stage> 范围,或 width/height 设为 0,会导致图形不可见。
    解决方案:检查坐标是否在 Stagewidth/height 范围内,确认尺寸属性大于 0。

  • 图片加载顺序问题:使用 <Image> 组件时,若图片未加载完成就传入 image 属性,会导致图片不显示。
    解决方案:通过 useEffect 监听图片加载完成后再渲染 <Image>

    import React, { useState, useEffect } from 'react';
    import { Stage, Layer, Image } from 'react-konva';
    
    const KonvaImage = ({ src }) => {
      const [image, setImage] = useState(null);
    
      useEffect(() => {
        const img = new Image();
        img.src = src;
        img.onload = () => setImage(img); // 加载完成后更新状态
      }, [src]);
    
      return image ? <Image image={image} width={200} height={150} /> : null;
    };
    

拖拽事件不生效

可能原因与解决方法:

  • 未设置 draggable={true}:图形元素默认不支持拖拽,需显式添加 draggable 属性。
    解决方案:在图形组件上添加 draggable,如 <Rect draggable />

  • 事件被上层元素遮挡:若图形上方有其他元素(如透明的 Rect),会导致拖拽事件被拦截。
    解决方案:通过 zIndex 属性调整图形层级(zIndex 越大,层级越高),或确保上层元素不拦截事件(设置 pointerEvents="none")。

  • 拖拽范围限制问题:若通过 dragBoundFunc 限制拖拽范围时逻辑错误,可能导致拖拽失效。
    解决方案:检查 dragBoundFunc 函数返回值是否正确(需返回 { x, y } 对象):

    <Rect
      draggable
      dragBoundFunc={(pos) => {
        // 限制拖拽范围在 Stage 内
        return {
          x: Math.max(0, Math.min(pos.x, 800 - 100)), // 800 是 Stage 宽度,100 是矩形宽度
          y: Math.max(0, Math.min(pos.y, 600 - 60))   // 600 是 Stage 高度,60 是矩形高度
        };
      }}
    />
    

大规模图形场景下性能卡顿

可能原因与解决方法:

  • 未做重渲染优化:父组件频繁重渲染导致所有图形组件连带重渲染。
    解决方案:参考第四章内容,使用 React.memo 缓存图形组件、useCallback 缓存事件函数。

  • Layer 数量过多或不合理:若每个图形都单独放在一个 Layer 中,会增加 Canvas 绘制开销。
    解决方案:合理拆分 Layer,将静态元素归为一个 Layer,动态元素归为一个或少数几个 Layer

  • 未启用可视区域裁剪:渲染了视图外的大量图形,浪费性能。
    解决方案:实现第四章提到的“可视区域裁剪”逻辑,仅渲染当前视图内的图形。

与 React状态同步延迟

可能原因与解决方法:

  • 直接操作 Konva 实例属性:若通过 e.target.setAttrs({ x: 100 }) 直接修改图形属性,未同步到 React 状态,会导致状态与视图不一致。
    解决方案:修改属性后,需同步更新 React 状态(如 onDragEnd 事件中更新 x/y 状态),确保状态是唯一数据源。

  • 动画导致的状态滞后:Konva 原生动画(如 Animate 组件)修改属性时,不会自动同步到 React 状态,导致状态滞后。
    解决方案:在动画结束后,通过 onFinish 事件同步状态:

    <Animate
      config={animationConfig}
      onFinish={() => {
        // 动画结束后同步状态到 React
        setRectX(400);
        setRectY(250);
      }}
    />
    

7. 版本兼容与升级要点

react-konva 与 React、Konva.js 的版本存在一定依赖关系,升级时需注意兼容性,避免出现 API 不兼容问题。

7.1. 版本依赖关系

react-konva 版本 支持 React 版本 依赖 Konva.js 版本
2.x 16.8+(支持 Hooks) 7.x
1.x 15.x - 16.x 6.x

注意react-konva@2.x 是目前的稳定版本,推荐使用,且需确保 konva 版本与 react-konva 兼容(通常安装时会自动匹配)。

7.2. 升级核心注意事项

  • 从 1.x 升级到 2.x

    1. react-konva@2.x 移除了 ReactKonvaCore 等旧 API,统一使用顶层导出组件(如 import { Stage } from 'react-konva');
    2. 不再支持 React 16.8 以下版本,需先升级 React 到 16.8+;
    3. Konva 实例获取方式变化:从 ref 获取时,需通过 ref.current 访问(如 stageRef.current),而非旧版的 ref 直接访问。
  • Konva.js 升级注意事项

    1. Konva.js 7.x 对事件系统做了优化,部分事件名称调整(如 dragmove 改为 drag),需同步修改事件处理函数;
    2. 图形属性 offset 不再支持数组形式(如 offset={[50, 30]}),需拆分为 offsetX={50}offsetY={30}

8. 总结

react-konva 作为 React 生态中成熟的 2D 图形库,其核心价值在于:

  • 低学习成本:使用 React 组件化思维操作图形,无需从零学习 Canvas 或 Konva.js 原生 API;
  • 高性能:基于 Konva.js 的分层渲染和事件优化,支持大规模图形场景;
  • 强扩展性:可与 React 生态工具(如 Redux、React Router)无缝集成,也可结合 d3.jschart.js 等库实现复杂功能;
  • 完善的生态:官方文档详细,社区活跃,问题解决资源丰富。

在选择之前,请了解它的能力边界,适用场景与不适用场景如下:

  • 适用场景

    • 交互式数据可视化(如散点图、流程图);
    • 轻量级设计工具(如简易海报编辑器、思维导图);
    • 2D 小游戏(如贪吃蛇、拼图);
    • 自定义图形组件(如仪表盘、进度条)。
  • 不适用场景

    • 3D 图形渲染(需使用 Three.js 等 3D 库);
    • 超大规模图形渲染(如百万级节点的地图,需使用 WebGL 优化的库);
    • 复杂的矢量图形编辑(需使用 SVG 或专业矢量库)。

通过本文的讲解,相信开发者已掌握 react-konva 的核心用法、性能优化技巧和实际应用场景。在实际项目中,建议结合具体需求选择合适的功能模块,灵活运用优化策略,构建高效、流畅的图形交互应用。

参考来源:


本次分享就到这儿啦,我是鹏多多,深耕前端的技术创作者,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~

PS:在本页按F12,在console中输入document.getElementsByClassName('panel-btn')[0].click();有惊喜哦~

往期文章

React瀑布流Masonry-Layout插件全方位指南:从基础到进阶实践

作者 鹏多多
2025年10月29日 08:35

1. 介绍

1.1. 瀑布流布局的核心特点

瀑布流布局不同于传统的网格布局(Grid Layout),它不要求所有元素保持统一的高度或宽度,而是根据元素自身的尺寸自动“填充”到容器中,形成类似“瀑布”的错落效果。其核心优势包括:

  • 视觉吸引力:非对称布局打破了传统网格的呆板,更符合现代设计美学;
  • 空间高效利用:避免因元素尺寸差异导致的大量空白区域,尤其适合图片、卡片等不规则内容;
  • 响应式适配:可根据屏幕宽度自动调整列数,适配移动端、平板和桌面端。

1.2. react-masonry-layout 的核心价值

原生 masonry 库(由 Desandro 开发)是实现瀑布流的经典工具,但直接在 React 项目中使用需要手动处理 DOM 操作、组件生命周期同步等问题。react-masonry-layout 作为其 React 封装版,解决了这些痛点:

  • 组件化封装:将瀑布流逻辑封装为 React 组件,支持 JSX 语法和 Props 配置;
  • 生命周期同步:自动关联 React 组件的挂载、更新、卸载过程,避免内存泄漏;
  • 状态驱动:支持通过 Props 动态修改布局参数(如列数、间距),无需手动调用 DOM 方法;
  • 生态兼容:可与 React 常用库(如 Redux、React Router)无缝配合,同时支持 TypeScript 类型提示。

2. 基础使用

2.1. 环境准备与安装

react-masonry-layout 依赖于原生 masonry 库,因此需要同时安装两个包。支持 npm 或 yarn 安装:

# npm 安装
npm install react-masonry-layout masonry-layout --save

# yarn 安装
yarn add react-masonry-layout masonry-layout

如果使用 TypeScript,还需安装类型声明文件(非官方维护,但社区支持良好):

npm install @types/react-masonry-layout @types/masonry-layout --save-dev

2.2. 最小化示例

下面通过一个简单的图片列表,演示 react-masonry-layout 的基础用法。核心是引入 Masonry 组件,并将需要布局的元素作为其子元素传入。

import React from 'react';
import Masonry from 'react-masonry-layout';

// 模拟图片数据(包含不同尺寸的图片URL)
const imageData = [
  { id: 1, url: 'https://picsum.photos/400/500', alt: 'Image 1' },
  { id: 2, url: 'https://picsum.photos/400/300', alt: 'Image 2' },
  { id: 3, url: 'https://picsum.photos/400/600', alt: 'Image 3' },
  { id: 4, url: 'https://picsum.photos/400/400', alt: 'Image 4' },
  { id: 5, url: 'https://picsum.photos/400/550', alt: 'Image 5' },
  { id: 6, url: 'https://picsum.photos/400/350', alt: 'Image 6' },
];

const BasicMasonry = () => {
  // 配置瀑布流参数
  const masonryOptions = {
    columnWidth: 400, // 每列的基础宽度(单位:px)
    gutter: 20, // 列与列、元素与元素之间的间距(单位:px)
    fitWidth: true, // 是否自动适配容器宽度(开启后会根据容器宽度调整列数)
    originLeft: true, // 从左侧开始布局(false 则从右侧开始)
  };

  return (
    <div style={{ maxWidth: '1200px', margin: '0 auto' }}>
      <h2>基础图片瀑布流</h2>
      {/* Masonry 组件:传入配置项和子元素 */}
      <Masonry
        options={masonryOptions}
        // 可选为每个子元素添加统一的类名方便样式控制className="masonry-container"
        // 可选为子元素的容器添加类名
        elementType="div"
      >
        {imageData.map((image) => (
          // 每个子元素需要唯一的 key
          <div key={image.id} className="masonry-item">
            <img
              src={image.url}
              alt={image.alt}
              style={{ width: '100%', borderRadius: '8px' }}
              // 关键确保图片加载完成后再触发布局避免尺寸计算错误onLoad={(e) => e.target.style.opacity = 1}
              style={{ width: '100%', borderRadius: '8px', opacity: 0, transition: 'opacity 0.3s' }}
            />
          </div>
        ))}
      </Masonry>
    </div>
  );
};

export default BasicMasonry;

关键说明:

  1. options Props:核心配置项,继承自原生 masonry 库,常用参数包括:

    • columnWidth:每列的基础宽度(可设为 CSS 选择器,如 .masonry-item,自动取第一个匹配元素的宽度);
    • gutter:元素之间的间距(支持数字或 CSS 选择器,如 .gutter-sizer);
    • fitWidth:开启后,瀑布流容器会自动调整宽度以适配父容器,适合响应式场景;
    • itemSelector:指定子元素的选择器(若子元素包含其他辅助元素,需通过此配置明确布局对象)。
  2. 图片加载问题:图片未加载完成时,其高度为 0,会导致布局错乱。解决方式包括:

    • 为图片添加 onLoad 事件,确保加载完成后再显示并触发布局;
    • 预先设置图片的宽高比(如使用 aspect-ratio CSS 属性);
    • 使用占位符(如骨架屏)临时填充空间。

3. 进阶用法

3.1. 动态数据:添加/删除元素

在实际项目中,瀑布流的内容往往是动态加载的(如滚动加载更多、筛选内容)。react-masonry-layout 支持通过修改子元素列表自动更新布局,无需手动调用刷新方法。

import React, { useState } from 'react';
import Masonry from 'react-masonry-layout';

const DynamicMasonry = () => {
  const [images, setImages] = useState(imageData); // 初始数据
  const [nextId, setNextId] = useState(7); // 下一个元素的ID

  // 配置:使用 CSS 选择器动态获取列宽(适合响应式)
  const masonryOptions = {
    itemSelector: '.masonry-item',
    columnWidth: '.masonry-sizer', // 用隐藏的 sizer 元素控制列宽
    gutter: 20,
    fitWidth: true,
  };

  // 添加新元素
  const addImage = () => {
    const newImage = {
      id: nextId,
      url: `https://picsum.photos/400/${Math.floor(Math.random() * 300) + 300}`, // 随机高度
      alt: `Image ${nextId}`,
    };
    setImages([...images, newImage]);
    setNextId(nextId + 1);
  };

  // 删除最后一个元素
  const removeImage = () => {
    if (images.length === 0) return;
    setImages(images.slice(0, -1));
  };

  return (
    <div style={{ maxWidth: '1200px', margin: '0 auto' }}>
      <div style={{ marginBottom: '20px' }}>
        <button onClick={addImage} style={{ marginRight: '10px', padding: '8px 16px' }}>
          添加图片
        </button>
        <button onClick={removeImage} style={{ padding: '8px 16px' }}>
          删除最后一张
        </button>
      </div>

      {/* 隐藏的 sizer 元素:用于动态控制列宽(响应式关键) */}
      <div className="masonry-sizer" style={{ width: 'calc(33.333% - 13.333px)' }}></div>
      {/* 可选:gutter 元素(若需更灵活的间距控制) */}
      <div className="gutter-sizer" style={{ width: '20px' }}></div>

      <Masonry options={masonryOptions} className="masonry-container">
        {images.map((image) => (
          <div key={image.id} className="masonry-item">
            <img
              src={image.url}
              alt={image.alt}
              style={{ width: '100%', borderRadius: '8px', opacity: 0, transition: 'opacity 0.3s' }}
              onLoad={(e) => e.target.style.opacity = 1}
            />
          </div>
        ))}
      </Masonry>
    </div>
  );
};

export default DynamicMasonry;

进阶技巧:

  • columnWidth 用 sizer 元素:通过隐藏的 .masonry-sizer 元素控制列宽,配合 CSS 百分比宽度(如 33.333% 对应 3 列),可实现响应式列数调整;
  • 动态更新的原理react-masonry-layout 会监听子元素列表的变化(通过 key 识别),自动触发 masonry 实例的 reloadItems()layout() 方法。

3.2. 响应式布局

实现响应式瀑布流的核心是根据屏幕宽度动态调整列数。react-masonry-layout 支持两种方式:

方式 1:使用 CSS Media Query 控制 sizer 元素宽度

通过隐藏的 .masonry-sizer 元素,结合 CSS 媒体查询动态修改其宽度,从而改变列数:

/* 全局样式 */
.masonry-container {
  margin: 0 auto;
}

/* sizer 元素:控制列宽 */
.masonry-sizer {
  width: calc(50% - 10px); /* 移动端默认 2 列 */
}

/* 平板设备(≥768px):3 列 */
@media (min-width: 768px) {
  .masonry-sizer {
    width: calc(33.333% - 13.333px);
  }
}

/* 桌面设备(≥1200px):4 列 */
@media (min-width: 1200px) {
  .masonry-sizer {
    width: calc(25% - 15px);
  }
}

/* 元素间距:与 gutter 配置一致 */
.masonry-item {
  margin-bottom: 20px;
}

在组件中只需引入样式,并保持 masonryOptionscolumnWidth: '.masonry-sizer' 即可。

方式 2:通过 JavaScript 动态计算列数

若需要更复杂的响应式逻辑(如根据父容器宽度而非屏幕宽度调整),可通过 useEffect 监听宽度变化,动态修改 columnWidth

import React, { useState, useEffect, useRef } from 'react';
import Masonry from 'react-masonry-layout';

const ResponsiveMasonry = () => {
  const containerRef = useRef(null);
  const [columnWidth, setColumnWidth] = useState(400); // 初始列宽

  // 监听容器宽度变化,动态计算列宽
  useEffect(() => {
    const calculateColumnWidth = () => {
      if (!containerRef.current) return;
      const containerWidth = containerRef.current.clientWidth;
      // 逻辑:容器宽度 ≥1200px → 4列;≥768px →3列;否则2列
      if (containerWidth >= 1200) {
        setColumnWidth(containerWidth / 4 - 15); // 减去间距
      } else if (containerWidth >= 768) {
        setColumnWidth(containerWidth / 3 - 13.333);
      } else {
        setColumnWidth(containerWidth / 2 - 10);
      }
    };

    // 初始计算
    calculateColumnWidth();
    // 监听窗口 resize 事件
    window.addEventListener('resize', calculateColumnWidth);
    // 清理事件监听
    return () => window.removeEventListener('resize', calculateColumnWidth);
  }, []);

  const masonryOptions = {
    columnWidth,
    gutter: 20,
    fitWidth: true,
  };

  return (
    <div ref={containerRef} style={{ maxWidth: '1400px', margin: '0 auto' }}>
      <Masonry options={masonryOptions} className="masonry-container">
        {imageData.map((image) => (
          <div key={image.id} className="masonry-item">
            <img src={image.url} alt={image.alt} style={{ width: '100%', borderRadius: '8px' }} />
          </div>
        ))}
      </Masonry>
    </div>
  );
};

export default ResponsiveMasonry;

3.3. 滚动加载更多

结合 react-intersection-observer 库(监听元素是否进入视口),可实现滚动到底部自动加载更多内容:

步骤 1:安装依赖

# npm 安装
npm install react-intersection-observer --save

# yarn 安装
yarn add react-intersection-observer

步骤 2:实现滚动加载逻辑

通过 useInView 钩子监听“加载更多”触发点(通常是列表底部的占位元素),当该元素进入视口时,自动请求新数据并追加到列表中:

import React, { useState, useEffect } from 'react';
import Masonry from 'react-masonry-layout';
import { useInView } from 'react-intersection-observer';

// 模拟接口请求:从服务器获取新图片数据
const fetchMoreImages = async (startId, count = 6) => {
  // 实际项目中替换为真实接口请求(如 axios.get)
  await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟网络延迟
  return Array.from({ length: count }, (_, i) => ({
    id: startId + i,
    url: `https://picsum.photos/400/${Math.floor(Math.random() * 300) + 300}`,
    alt: `Image ${startId + i}`
  }));
};

const InfiniteScrollMasonry = () => {
  const [images, setImages] = useState(imageData); // 初始数据
  const [nextId, setNextId] = useState(7); // 下一批数据的起始ID
  const [isLoading, setIsLoading] = useState(false); // 加载状态锁(防止重复请求)
  const [hasMore, setHasMore] = useState(true); // 是否还有更多数据(模拟分页终止条件)

  // 配置 Intersection Observer:监听底部触发点
  const { ref: loadTriggerRef, inView } = useInView({
    threshold: 0.1, // 元素 10% 进入视口时触发
    triggerOnce: false, // 允许重复触发(每次滚动到底部都可触发)
  });

  // 瀑布流配置
  const masonryOptions = {
    columnWidth: '.masonry-sizer',
    gutter: 20,
    fitWidth: true,
  };

  // 监听 inView 状态:当触发点进入视口且无加载中时,请求新数据
  useEffect(() => {
    if (inView && !isLoading && hasMore) {
      loadMoreImages();
    }
  }, [inView, isLoading, hasMore]);

  // 加载更多数据的核心函数
  const loadMoreImages = async () => {
    setIsLoading(true); // 开启加载锁
    try {
      const newImages = await fetchMoreImages(nextId);
      // 模拟“无更多数据”场景(如加载到第 30 张后停止)
      if (nextId + newImages.length > 30) {
        setHasMore(false);
      }
      setImages(prev => [...prev, ...newImages]); // 追加新数据
      setNextId(prev => prev + newImages.length); // 更新下一批起始ID
    } catch (error) {
      console.error('Failed to load more images:', error);
    } finally {
      setIsLoading(false); // 关闭加载锁(无论成功/失败都需释放)
    }
  };

  return (
    <div style={{ maxWidth: '1200px', margin: '0 auto' }}>
      <h2>滚动加载瀑布流</h2>
      
      {/* 隐藏的 sizer 元素(配合 CSS 实现响应式列数) */}
      <div className="masonry-sizer" style={{ width: 'calc(33.333% - 13.333px)' }}></div>
      
      <Masonry options={masonryOptions} className="masonry-container">
        {images.map((image) => (
          <div key={image.id} className="masonry-item">
            <img
              src={image.url}
              alt={image.alt}
              style={{ width: '100%', borderRadius: '8px', opacity: 0, transition: 'opacity 0.3s' }}
              onLoad={(e) => e.target.style.opacity = 1}
            />
          </div>
        ))}
        
        {/* 加载状态提示(位于列表底部) */}
        <div ref={loadTriggerRef} style={{ height: '50px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
          {isLoading ? '加载中...' : hasMore ? '滚动到底部加载更多' : '已加载全部内容'}
        </div>
      </Masonry>
    </div>
  );
};

export default InfiniteScrollMasonry;

注意事项

  • 加载锁(isLoading:必须通过状态锁防止滚动时触发重复请求(例如用户快速滚动到底部,避免同时发起多个接口调用);
  • 终止条件(hasMore:根据实际业务逻辑设置(如接口返回“无更多数据”标识、达到固定数据量上限),避免无限请求;
  • 性能优化:可通过 throttle(节流)或调整 threshold(触发阈值)减少 inView 事件的触发频率,尤其在数据量大时。

4. 性能优化与常见问题

4.1. 性能优化策略

当瀑布流中元素数量较多(如数百个图片卡片)时,可能出现加载缓慢、滚动卡顿等问题,可通过以下策略优化:

4.1.1. 图片懒加载

仅加载“进入视口”的图片,减少初始加载的资源量。可结合 react-lazyload 库或原生 loading="lazy" 属性实现:

方案 1:使用原生 loading="lazy"(简单高效,兼容性良好)
<img
  src={image.url}
  alt={image.alt}
  style={{ width: '100%', borderRadius: '8px' }}
  loading="lazy" // 原生懒加载:仅当图片接近视口时加载
  decoding="async" // 异步解码图片,避免阻塞主线程
/>
方案 2:使用 react-lazyload(支持更精细的控制)
  1. 安装依赖:
npm install react-lazyload --save
  1. 组件中使用:
import LazyLoad from 'react-lazyload';

// 在 Masonry 子元素中包裹 LazyLoad
<div key={image.id} className="masonry-item">
  <LazyLoad
    height={200} // 占位高度避免布局跳动offset={100} // 提前 100px 开始加载
    once // 仅加载一次滚动回滚时不重复加载)
  >
    <img
      src={image.url}
      alt={image.alt}
      style={{ width: '100%', borderRadius: '8px' }}
    />
  </LazyLoad>
</div>

4.1.2. 虚拟滚动(大数据量场景)

当元素数量超过 500 个时,即使使用懒加载,DOM 节点过多仍会导致页面卡顿。此时可结合 虚拟滚动 技术,仅渲染“当前视口可见”的元素,大幅减少 DOM 数量。

推荐使用 react-windowreact-virtualized 库,与 react-masonry-layout 配合实现:

import { FixedSizeList as List } from 'react-window';
import Masonry from 'react-masonry-layout';

const VirtualizedMasonry = () => {
  // 虚拟滚动列表:仅渲染视口内的元素
  const renderMasonryItems = ({ index, style }) => {
    const image = images[index];
    return (
      <div key={image.id} style={style} className="masonry-item">
        <img src={image.url} alt={image.alt} style={{ width: '100%' }} />
      </div>
    );
  };

  return (
    <div style={{ maxWidth: '1200px', margin: '0 auto' }}>
      {/* 虚拟滚动容器:高度固定,仅渲染视口内元素 */}
      <List
        height={800} // 容器高度
        width="100%" // 容器宽度
        itemCount={images.length} // 总元素数量
        itemSize={300} // 每个元素的预估高度可动态调整)
      >
        {renderMasonryItems}
      </List>
      
      {/* 瀑布流布局:基于虚拟滚动的结果进行排版 */}
      <Masonry options={masonryOptions} className="masonry-container">
        {/* 虚拟滚动渲染的元素会自动注入此处 */}
      </Masonry>
    </div>
  );
};

4.1.3. 减少布局重排(Reflow)

瀑布流的核心是“计算元素位置并布局”,频繁的布局重排会严重影响性能。优化方式:

  • 预先固定元素宽高比:图片加载前通过 aspect-ratio CSS 属性设置宽高比(如 aspect-ratio: 4/5),避免加载后高度变化导致重排;
  • 批量更新数据:动态添加元素时,尽量批量操作(如一次添加 6 张图片,而非单张添加),减少 react-masonry-layout 触发布局的次数;
  • 避免实时修改样式:尽量通过 CSS 类切换样式,而非直接修改 style 属性(浏览器对类的处理更高效)。

4.2. 常见问题与解决方案

问题描述 根本原因 解决方案
图片加载完成后布局错乱 图片未加载时高度为 0,masonry 基于错误高度计算布局 1. 为图片添加 onLoad 事件,加载完成后调用 masonry.layout()
2. 使用 aspect-ratio 预先设置宽高比;
3. 加载前显示与图片比例一致的占位符
动态添加元素后布局未更新 未正确监听子元素列表变化,或 key 重复导致 React 未识别元素更新 1. 确保每个子元素的 key 唯一且稳定(如使用数据 ID,而非索引);
2. 若手动操作 DOM,需调用 masonry.reloadItems() + masonry.layout() 强制刷新
响应式列数切换时元素重叠 窗口 resize 后,masonry 未重新计算列宽和元素位置 1. 监听 window.resize 事件,触发 masonry.layout()
2. 使用 CSS Media Query 控制 masonry-sizer 宽度,让 masonry 自动适配
移动端滚动卡顿 1. 图片未懒加载,资源加载阻塞主线程;
2. DOM 节点过多,重排成本高
1. 开启图片懒加载(原生或第三方库);
2. 对大数据量场景使用虚拟滚动;
3. 为图片添加 will-change: transform 提示浏览器优化渲染
TypeScript 类型报错 未安装类型声明文件,或类型定义与实际 Props 不匹配 1. 安装 @types/react-masonry-layout@types/masonry-layout
2. 若类型不完整,可手动扩展接口(如 interface CustomMasonryProps extends MasonryProps { ... }

5. 总结

react-masonry-layout 作为原生 masonry 库的 React 封装,核心价值在于组件化整合生命周期同步,让开发者无需关注底层 DOM 操作,即可快速实现高质量瀑布流布局。其优势与适用场景:

  • 优势:配置简单、生态兼容好(支持 Redux/TypeScript)、动态更新能力强;
  • 适用场景:图片画廊、商品列表、内容卡片等不规则尺寸元素的排版(如电商 App 商品页、设计社区作品展示)。

使用时需重点关注图片加载顺序(避免布局错乱)、响应式适配(确保多端体验一致)和性能优化(大数据量场景需懒加载/虚拟滚动)。


本次分享就到这儿啦,我是鹏多多,深耕前端的技术创作者,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~

PS:在本页按F12,在console中输入document.getElementsByClassName('panel-btn')[0].click();有惊喜哦~

往期文章

昨天以前首页

深度解析React中useEffect钩子的使用

作者 鹏多多
2025年10月27日 08:38

1. 前言

在React函数式组件的开发中,useEffect是一个核心钩子,它为我们提供了在组件渲染后执行副作用操作的能力。副作用操作包括数据获取、订阅、手动DOM操作等。整体类似于Vue中的生命周期+计算属性。下面,将深入探讨useEffect的工作原理、常见应用场景、性能优化以及潜在的陷阱。

2. 基本概念和语法

useEffect是React提供的一个钩子函数,用于在函数式组件中执行副作用操作。其基本语法如下:

useEffect(callback, dependencies);
  • callback:副作用函数,在组件渲染后执行。可以返回一个清理函数,用于在组件卸载前执行清理操作。
  • dependencies(可选):依赖项数组,用于控制副作用函数的执行时机。如果省略该参数,副作用函数将在每次渲染后执行;如果传入空数组[],副作用函数仅在首次渲染后执行;如果传入具体的依赖项,副作用函数将在依赖项变化时执行。

useEffect的执行时机是在浏览器完成DOM渲染之后,但在屏幕更新之前。这意味着副作用操作不会阻塞浏览器的渲染过程,从而保证了应用的流畅性。

3. 核心应用场景

下面是一些核心应用场景,每个都会有一个例子:

3.1. 数据获取

在组件加载时获取数据是一个常见的需求,使用useEffect可以轻松实现这一点:

import React, { useState, useEffect } from 'react';

function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/data');
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []); // 空依赖数组确保副作用只在首次渲染后执行

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <div>
      {data && data.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

3.2. DOM操作与动画

useEffect可以用于执行DOM操作,比如设置焦点、调整元素尺寸等:

import React, { useRef, useEffect } from 'react';

function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus(); // 在组件挂载后自动聚焦输入框
  }, []); // 空依赖数组确保副作用只在首次渲染后执行

  return <input ref={inputRef} type="text" />;
}

3.3. 订阅与取消订阅

当需要监听事件或订阅外部数据源时,可以在useEffect中设置订阅,并在清理函数中取消订阅:

import React, { useState, useEffect } from 'react';

function WindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });

  useEffect(() => {
    const handleResize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };

    window.addEventListener('resize', handleResize);

    // 清理函数在组件卸载前执行
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空依赖数组确保只添加一次事件监听器

  return (
    <div>
      Window size: {size.width} x {size.height}
    </div>
  );
}

3.4. 计时器与间隔执行

使用useEffect可以设置计时器或间隔执行的任务:

import React, { useState, useEffect } from 'react';

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);

    // 清理函数在组件卸载前执行
    return () => {
      clearInterval(interval);
    };
  }, []); // 空依赖数组确保只设置一次计时器

  return <div>Count: {count}</div>;
}

4. 依赖项数组的使用与优化

useEffect的依赖项数组是控制副作用执行时机的关键。合理使用依赖项数组可以避免不必要的副作用执行,从而优化性能。

4.1. 空依赖数组

当依赖项数组为空时,副作用函数仅在首次渲染后执行,类似于componentDidMount

useEffect(() => {
  // 只在组件挂载时执行
  console.log('Component mounted');

  return () => {
    // 只在组件卸载时执行
    console.log('Component will unmount');
  };
}, []);

4.2. 包含依赖项的数组

当依赖项数组包含值时,副作用函数将在依赖项变化时执行,类似于componentDidUpdate

useEffect(() => {
  // 每次count变化时执行
  console.log(`Count changed to: ${count}`);
}, [count]); // 依赖于count

4.3. 省略依赖项数组

如果省略依赖项数组,副作用函数将在每次渲染后执行,包括首次渲染和后续更新:

useEffect(() => {
  // 每次渲染后都执行
  console.log('Rendered');
});

4.4. 使用复杂依赖项

当依赖项是对象、数组或函数时,需要特别注意引用相等性问题。可以使用useCallbackuseMemo来确保依赖项的稳定性:

import React, { useState, useEffect, useCallback } from 'react';

function ComplexDependency() {
  const [data, setData] = useState([]);
  
  // 使用useCallback缓存函数,避免每次渲染时创建新函数
  const fetchData = useCallback(async () => {
    const response = await fetch('https://api.example.com/data');
    const result = await response.json();
    setData(result);
  }, []); // 空依赖数组确保fetchData引用不变

  useEffect(() => {
    fetchData();
  }, [fetchData]); // 依赖于fetchData的引用

  return <div>Data loaded: {data.length}</div>;
}

5. 处理异步操作的陷阱与解决方案

useEffect中处理异步操作时,需要注意避免常见的陷阱,比如竞态条件和内存泄漏。

5.1. 竞态条件问题

当在useEffect中执行异步操作时,如果在请求完成前组件已经卸载,可能会导致内存泄漏和错误。可以使用一个标记来跟踪组件的挂载状态:

import React, { useState, useEffect } from 'react';

function SafeDataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let isMounted = true; // 跟踪组件挂载状态

    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/data');
        const result = await response.json();
        
        // 只有在组件仍然挂载时才更新状态
        if (isMounted) {
          setData(result);
          setLoading(false);
        }
      } catch (err) {
        if (isMounted) {
          console.error(err);
          setLoading(false);
        }
      }
    };

    fetchData();

    // 清理函数在组件卸载前执行
    return () => {
      isMounted = false;
    };
  }, []);

  return loading ? <div>Loading...</div> : <div>{data}</div>;
}

5.2. 使用AbortController取消请求

对于基于Fetch API的请求,可以使用AbortController来取消未完成的请求,Axios请求也同理。下面是一个Fetch的取消示例:

useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal;

  const fetchData = async () => {
    try {
      const response = await fetch('https://api.example.com/data', { signal });
      const result = await response.json();
      setData(result);
    } catch (err) {
      if (err.name === 'AbortError') {
        console.log('Request aborted');
      } else {
        console.error(err);
      }
    }
  };

  fetchData();

  // 组件卸载前取消请求
  return () => controller.abort();
}, []);

6. useEffect与类组件生命周期的对比

在类组件中,副作用操作通常分散在多个生命周期方法中,如componentDidMountcomponentDidUpdatecomponentWillUnmount。而useEffect将这些操作统一到一个API中:

类组件生命周期 useEffect 等价写法
componentDidMount useEffect(() => { /* 初始化 */ }, [])
componentDidUpdate useEffect(() => { /* 更新 */ })
componentWillUnmount useEffect(() => { return () => { /* 清理 */ } }, [])

7. 最佳实践与注意事项

  1. 保持副作用函数的纯净性:副作用函数应该只执行必要的操作,避免在其中执行会影响渲染结果的计算。

  2. 避免无限循环:确保依赖项数组正确包含所有需要监听的变量,避免因依赖项缺失导致的无限循环。

  3. 使用多个useEffect:将不相关的副作用分离到不同的useEffect中,提高代码的可读性和可维护性。

  4. 谨慎使用空依赖数组:只有在确实只需要在组件挂载和卸载时执行副作用时才使用空依赖数组。

  5. 使用useRef存储可变值:如果需要在副作用中访问之前的值,可以使用useRef来存储这些值。

8. Vue中对应的useEffect实现

在 Vue 中,与 React 的useEffect最接近的功能是通过生命周期钩子和计算属性组合实现的。Vue 提供了更细分的生命周期钩子来处理不同阶段的副作用,而不是单一的 API。以下是具体的对应关系和实现方式:

在Vue中,与React的useEffect最接近的功能是通过生命周期钩子计算属性组合实现的。Vue提供了更细分的生命周期钩子来处理不同阶段的副作用,而不是单一的API。以下是具体的对应关系和实现方式:

8.1. Vue2的类似实现

React useEffect场景 Vue 生命周期钩子/方法
组件挂载后执行(类似useEffect(() => {}, []) onMounted(组合式API)或mounted(选项式API)
组件更新后执行(类似useEffect(() => {}) onUpdated(组合式API)或updated(选项式API)
组件卸载前清理(类似useEffect(() => () => {...}, []) onBeforeUnmount/onUnmounted(组合式API)或beforeDestroy/destroyed(选项式API)

8.2. Vue3的类似实现

Vue 3提供了watchEffect API,它会自动追踪依赖并在初始化和依赖变化时执行,更接近React的useEffect

import { ref, watchEffect } from 'vue';

export default {
  setup() {
    const count = ref(0);
    
    // 自动追踪count的变化
    watchEffect(() => {
      console.log(`Count is: ${count.value}`);
      // 副作用逻辑(会在初始化和count变化时执行)
      
      // 返回清理函数(在组件卸载前或下次执行前调用)
      return () => {
        console.log('Cleaning up...');
      };
    });
    
    return { count };
  }
};

区别

  • watchEffect自动追踪依赖(通过访问响应式数据),而useEffect需要手动指定依赖数组。
  • watchEffect立即执行一次(类似useEffect(() => {}, [])),而useEffect默认在首次渲染后执行。
React useEffect Vue 3 等效实现方式
首次渲染后执行 onMountedwatchEffect
依赖变化时执行 watchwatchEffect
组件卸载前清理 onUnmountedwatchEffect 的返回值
每次渲染后执行 onUpdated
自动追踪依赖 watchEffect

Vue通过更细分的生命周期钩子和响应式API提供了比useEffect更精细的控制,但核心思想都是处理副作用依赖变化。根据具体场景,你可以选择最合适的Vue API来替代useEffect的功能。

9. 总结

useEffect是React函数式组件中一个强大且灵活的钩子,它使我们能够在组件渲染后执行各种副作用操作。通过合理使用依赖项数组和清理函数,我们可以优化性能、避免内存泄漏,并确保代码的健壮性。


本次分享就到这儿啦,我是鹏多多,深耕前端的技术创作者,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~

PS:在本页按F12,在console中输入document.getElementsByClassName('panel-btn')[0].click();有惊喜哦~

往期文章

React动画方案对比:CSS动画和Framer Motion和React Spring

作者 鹏多多
2025年10月20日 08:38

1. 前言

在现代 Web 应用中,动画是提升用户体验的重要手段。React 生态中提供了多种动画实现方案,每种方案都有其适用场景和技术特点。本文将深入对比三种主流方案:原生 CSS 动画、Framer Motion 和 React Spring,通过实现同一动画效果展示各方案的优缺点,帮助你在项目中做出最佳选择。

2. CSS 动画

CSS 动画是实现简单过渡效果的最直接方式,无需引入额外依赖,性能表现优秀。

2.1. 基本实现方式

通过 CSS 类切换或 @keyframes 实现动画:

import React, { useState } from 'react';

function FadeInComponent() {
  const [show, setShow] = useState(false);
  
  const toggle = () => {
    setShow(!show);
  };
  
  return (
    <div>
      <button onClick={toggle}>显示/隐藏</button>
      <div 
        className={`fade-element ${show ? 'visible' : 'hidden'}`}
      >
        渐显渐隐元素
      </div>
    </div>
  );
}

// CSS 样式
.fade-element {
  opacity: 0;
  transition: opacity 0.5s ease;
}

.fade-element.visible {
  opacity: 1;
}

2.2. 复杂动画实现

使用 @keyframes 实现更复杂的动画效果:

@keyframes slideIn {
  0% { transform: translateX(-100%); opacity: 0; }
  100% { transform: translateX(0); opacity: 1; }
}

.slide-in {
  animation: slideIn 0.5s forwards;
}

2.3. 优缺点分析

  • 优点

    • 实现简单,无需额外学习成本
    • 性能最优(由浏览器直接优化)
    • 适合简单的过渡效果(如淡入淡出、缩放)
  • 缺点

    • 缺乏 JavaScript 控制能力(如暂停、反向播放)
    • 复杂动画(如物理动效)实现困难
    • 状态管理复杂(需维护多个 CSS 类)

3. Framer Motion

Framer Motion 是专为 React 设计的动画库,提供了强大的 API 和直观的组件化接口,适合复杂交互场景。

3.1. 基础使用

下面是一个简单过渡动画:

import { motion } from 'framer-motion';
import React, { useState } from 'react';

function FadeInComponent() {
  const [show, setShow] = useState(false);
  
  return (
    <div>
      <button onClick={() => setShow(!show)}>显示/隐藏</button>
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: show ? 1 : 0 }}
        transition={{ duration: 0.5 }}
      >
        渐显渐隐元素
      </motion.div>
    </div>
  );
}

3.2. 复杂动画

下面是一个拖拽与弹簧效果:

import { motion, useDragControls, useSpring } from 'framer-motion';

function DraggableBox() {
  const dragControls = useDragControls();
  const { x, y } = useSpring({
    x: 0,
    y: 0,
    config: { tension: 200, damping: 20 }
  });
  
  return (
    <motion.div
      drag
      dragControls={dragControls}
      dragConstraints={{ left: 0, right: 300, top: 0, bottom: 200 }}
      style={{ x, y }}
    >
      可拖拽元素
    </motion.div>
  );
}

3.3. 路由过渡动画

import { motion, AnimatePresence } from 'framer-motion';
import { Routes, Route, useLocation } from 'react-router-dom';

function AnimatedRoutes() {
  const location = useLocation();
  
  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={location.pathname}
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -20 }}
        transition={{ duration: 0.3 }}
      >
        <Routes location={location}>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </motion.div>
    </AnimatePresence>
  );
}

3.4. 优缺点分析

  • 优点

    • 功能全面,支持复杂动画(如拖拽、滚动触发、3D变换)
    • 声明式 API,代码简洁易维护
    • 良好的类型支持和文档
    • 支持与 React 生命周期深度集成
  • 缺点

    • 包体积较大(约 14KB gzipped)
    • 学习曲线较陡(需理解各种动画概念)
    • 性能略低于原生 CSS 动画

4. React Spring

React Spring 专注于实现自然流畅的物理动效,适合需要精细控制动画物理学特性的场景。

4.1. 基础使用

下面是一个弹簧动画:

import { useSpring, animated } from 'react-spring';

function SpringButton() {
  const props = useSpring({
    from: { opacity: 0, transform: 'scale(0.8)' },
    to: { opacity: 1, transform: 'scale(1)' },
    config: { tension: 170, friction: 26 }
  });
  
  return (
    <animated.button style={props}>
      弹簧按钮
    </animated.button>
  );
}

4.2. 交互触发动画

import { useSpring, useTrail, animated } from 'react-spring';

function TrailAnimation() {
  const trail = useTrail(5, {
    from: { opacity: 0, transform: 'translate3d(0,-40px,0)' },
    to: { opacity: 1, transform: 'translate3d(0,0,0)' },
  });
  
  return (
    <div>
      {trail.map((style, index) => (
        <animated.div key={index} style={style}>
          Item {index + 1}
        </animated.div>
      ))}
    </div>
  );
}

4.3. 滚动触发动画

import { useScroll, animated } from 'react-spring';

function ScrollAnimation() {
  const { scrollYProgress } = useScroll();
  
  return (
    <animated.div 
      style={{
        opacity: scrollYProgress,
        transform: scrollYProgress.interpolate(
          (y) => `translate3d(0, ${y * 50}px, 0)`
        )
      }}
    >
      滚动触发动画
    </animated.div>
  );
}

4.4. 优缺点分析

  • 优点

    • 专注于物理动效,提供丰富的物理学参数配置
    • 性能优秀,适合高频动画(如滚动、拖拽)
    • 轻量级(约 8KB gzipped)
    • 支持与其他库(如 Three.js)集成
  • 缺点

    • API 相对底层,学习成本较高
    • 缺乏内置组件(如 Framer Motion 的 AnimatePresence
    • 文档和社区资源不如 Framer Motion 完善

5. 性能对比与场景选择

性能对比如下:

方案 体积(gzipped) 简单动画性能 复杂动画性能
CSS 动画 0KB ✅✅✅
Framer Motion ~14KB ✅✅ ✅✅✅
React Spring ~8KB ✅✅ ✅✅✅

场景选择如下:

  • 推荐使用 CSS 动画的场景

    • 简单的过渡效果(如淡入淡出、悬停效果)
    • 无需 JavaScript 控制的纯视觉动画
    • 性能敏感的高频动画(如滚动指示器)
  • 推荐使用 Framer Motion 的场景

    • 复杂交互驱动的动画(如拖拽、缩放、路由过渡)
    • 需要丰富的布局动画(如列表项进入/退出动画)
    • 与 React 组件深度集成的动画
  • 推荐使用 React Spring 的场景

    • 需要精细控制物理参数的动画(如弹簧、阻尼效果)
    • 轻量级应用,对包体积敏感
    • 与其他动画库或 3D 库结合使用

6. 实战案例

下面通过实现一个模态框动画,对比三种方案的实现差异:

6.1. CSS 动画实现

// 组件代码
function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;
  
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        {children}
        <button onClick={onClose}>关闭</button>
      </div>
    </div>
  );
}

// CSS 样式
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  opacity: 0;
  animation: fadeIn 0.3s forwards;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 8px;
  transform: scale(0.8);
  animation: scaleIn 0.3s forwards;
}

@keyframes fadeIn {
  to { opacity: 1; }
}

@keyframes scaleIn {
  to { transform: scale(1); }
}

6.2. Framer Motion 实现

import { motion } from 'framer-motion';

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;
  
  return (
    <motion.div
      className="modal-overlay"
      onClick={onClose}
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      <motion.div
        className="modal-content"
        onClick={(e) => e.stopPropagation()}
        initial={{ scale: 0.8 }}
        animate={{ scale: 1 }}
        exit={{ scale: 0.8 }}
        transition={{ duration: 0.3 }}
      >
        {children}
        <button onClick={onClose}>关闭</button>
      </motion.div>
    </motion.div>
  );
}

6.3. React Spring 实现

import { useSpring, animated } from 'react-spring';

function Modal({ isOpen, onClose, children }) {
  const { opacity, scale } = useSpring({
    opacity: isOpen ? 1 : 0,
    scale: isOpen ? 1 : 0.8,
    config: { duration: 300 },
  });
  
  if (!isOpen) return null;
  
  return (
    <animated.div
      className="modal-overlay"
      onClick={onClose}
      style={{ opacity }}
    >
      <animated.div
        className="modal-content"
        onClick={(e) => e.stopPropagation()}
        style={{ transform: scale.interpolate(s => `scale(${s})`) }}
      >
        {children}
        <button onClick={onClose}>关闭</button>
      </animated.div>
    </animated.div>
  );
}

7. 总结

选择合适的动画方案对 React 应用的性能和用户体验至关重要。本文通过对比分析得出以下结论:

  1. CSS 动画:简单、高效,适合无交互的基础动画,是轻量级应用的首选。
  2. Framer Motion:功能全面、API 友好,适合复杂交互场景和大型应用。
  3. React Spring:专注物理动效,适合需要精细控制动画物理学特性的场景。

在实际项目中,建议根据动画复杂度、性能需求和团队技术栈综合选择。对于大多数场景,Framer Motion 提供了最佳的平衡;而对于追求极致性能的简单动画,CSS 动画仍是最优解。


本次分享就到这儿啦,我是鹏多多,深耕前端的技术创作者,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~

PS:在本页按F12,在console中输入document.getElementsByClassName('panel-btn')[0].click();有惊喜哦~

往期文章

❌
❌