NextJS Canvas拖拽SVG元素时出现闪烁问题求助
解决NextJS Canvas拖拽元素闪烁问题
核心问题分析
- 无图片缓存:每次
draw()都会重新创建Image对象并加载SVG,导致绘制延迟,加剧闪烁 - 状态更新不规范:直接修改
tablesState数组元素,未触发React状态更新,绘制逻辑异常 - 无缓冲机制:直接在画布清空重绘,视觉上出现空白间隙引发闪烁
具体解决方案
1. 预缓存SVG图片
提前生成并缓存所有形状的SVG图片,避免每次绘制重复加载:
// 组件内或外部维护缓存对象 const svgCache = {}; // 生成对应形状的SVG Base64内容 function generateSVG(shape, tableNbr, nbrPlaces) { let svgContent = ''; switch(shape) { case "square": svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140"> <rect x="20" y="20" fill="#CCC" width="100" height="100"/> <text x="70" y="70" text-anchor="middle" alignment-baseline="middle" fill="#000" font-size="26px">${tableNbr}</text> <circle cx="115" cy="25" r="25" fill="#000"/> <text x="115" y="27" text-anchor="middle" alignment-baseline="middle" fill="#FFF" font-weight="700" font-size="24px">-/${nbrPlaces}</text> </svg>`; break; case "oval": svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="190" height="140"> <ellipse cx="100" cy="70" rx="80" ry="50" fill="#CCC"/> <text x="100" y="70" text-anchor="middle" alignment-baseline="middle" fill="#000" font-size="26px">${tableNbr}</text> <circle cx="160" cy="30" r="25" fill="#000"/> <text x="160" y="30" text-anchor="middle" alignment-baseline="middle" fill="#FFF" font-weight="700" font-size="24px">-/${nbrPlaces}</text> </svg>`; break; case "rectangular": svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="210" height="140"> <rect width="150" height="100" x="20" y="20" fill="#CCC"/> <text x="95" y="70" text-anchor="middle" alignment-baseline="middle" fill="#000" font-size="26px">${tableNbr}</text> <circle cx="170" cy="30" r="25" fill="#000"/> <text x="170" y="30" text-anchor="middle" alignment-baseline="middle" fill="#FFF" font-weight="700" font-size="24px">-/${nbrPlaces}</text> </svg>`; break; default: // circle svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140"> <circle cx="70" cy="70" r="50" fill="#CCC"/> <text x="70" y="70" text-anchor="middle" alignment-baseline="middle" fill="#000" font-size="26px">${tableNbr}</text> <circle cx="110" cy="35" r="25" fill="#000"/> <text x="110" y="35" text-anchor="middle" alignment-baseline="middle" fill="#FFF" font-weight="700" font-size="24px">-/${nbrPlaces}</text> </svg>`; } return `data:image/svg+xml;base64,${window.btoa(svgContent)}`; } // 获取缓存的图片对象 function getCachedImage(el) { const key = `${el.shape}-${el.tableNbr}-${el.nbrPlaces}`; if (!svgCache[key]) { const img = new Image(); img.src = generateSVG(el.shape, el.tableNbr, el.nbrPlaces); svgCache[key] = img; } return svgCache[key]; }
2. 规范React状态更新
禁止直接修改tablesState元素,使用setTablesState触发状态更新:
const handleMouseMove = (event) => { if (!isDragging || selectedElement === -1) return; event.preventDefault(); const element = tablesState.find((el) => el.id === selectedElement); if (!element) return; const width = element.shape === "square" || element.shape === "circle" ? 80 : 150; const height = element.shape === "square" || element.shape === "circle" ? 80 : 100; // 正确更新状态 setTablesState(prevState => prevState.map(item => { if (item.id === selectedElement) { return {...item, x: event.offsetX - width / 2, y: event.offsetY - height / 2}; } return item; })); };
3. 实现双缓冲消除闪烁
创建离屏Canvas,先完成所有绘制再一次性渲染到主画布:
let offscreenCanvas = null; let offscreenCtx = null; useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; // 初始化离屏画布 offscreenCanvas = document.createElement('canvas'); offscreenCanvas.width = canvas.width; offscreenCanvas.height = canvas.height; offscreenCtx = offscreenCanvas.getContext('2d'); }, [canvasRef]); function draw() { if (!offscreenCtx || !context) return; // 清空离屏画布 offscreenCtx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height); for (const el of tablesState) { const img = getCachedImage(el); if (img.complete) { const [w, h] = el.shape === "square" || el.shape === "circle" ? [80, 80] : el.shape === "oval" ? [140, 100] : [150, 100]; offscreenCtx.drawImage(img, el.x, el.y, w, h); } } // 一次性绘制到主画布 context.drawImage(offscreenCanvas, 0, 0); }
4. 优化绘制触发时机
将draw绑定到tablesState状态更新,移除拖拽时手动调用:
useEffect(() => { if (!offscreenCtx || tablesState.length === 0) return; draw(); }, [tablesState, offscreenCtx]);
修正后的核心组件代码
import { useState, useEffect, useRef } from 'react'; export default function CanvasTable() { const canvasRef = useRef(null); const [tablesState, setTablesState] = useState([]); const isDraggingRef = useRef(false); const selectedElementRef = useRef(-1); let offscreenCanvas = null; let offscreenCtx = null; const svgCache = {}; // 省略generateSVG、getCachedImage、draw函数... useEffect(() => { // 假设tables是传入的初始数据 setTablesState(tables); }, []); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; offscreenCanvas = document.createElement('canvas'); offscreenCanvas.width = canvas.width; offscreenCanvas.height = canvas.height; offscreenCtx = offscreenCanvas.getContext('2d'); const context = canvas.getContext('2d'); const handleMouseDown = (event) => { event.preventDefault(); isDraggingRef.current = true; const prevX = event.offsetX; const prevY = event.offsetY; // 命中检测 for (let i = 0; i < tablesState.length; i++) { const el = tablesState[i]; const height = el.shape === "square" || el.shape === "circle" ? 80 : 100; const width = el.shape === "square" || el.shape === "circle" ? 70 : 150; if (prevX >= el.x && prevX <= el.x + width && prevY >= el.y && prevY <= el.y + height) { selectedElementRef.current = el.id; break; } } }; const handleMouseUp = (e) => { e.preventDefault(); isDraggingRef.current = false; selectedElementRef.current = -1; }; const handleMouseOut = (e) => { e.preventDefault(); isDraggingRef.current = false; selectedElementRef.current = -1; }; const handleMouseMove = (event) => { if (!isDraggingRef.current || selectedElementRef.current === -1) return; event.preventDefault(); const element = tablesState.find((el) => el.id === selectedElementRef.current); if (!element) return; const width = element.shape === "square" || element.shape === "circle" ? 80 : 150; const height = element.shape === "square" || element.shape === "circle" ? 80 : 100; setTablesState(prev => prev.map(el => { if (el.id === selectedElementRef.current) { return {...el, x: event.offsetX - width / 2, y: event.offsetY - height / 2}; } return el; })); }; canvas.addEventListener("mousedown", handleMouseDown); canvas.addEventListener("mouseup", handleMouseUp); canvas.addEventListener("mousemove", handleMouseMove); canvas.addEventListener("mouseout", handleMouseOut); return () => { canvas.removeEventListener("mousedown", handleMouseDown); canvas.removeEventListener("mouseup", handleMouseUp); canvas.removeEventListener("mousemove", handleMouseMove); canvas.removeEventListener("mouseout", handleMouseOut); }; }, [tablesState]); useEffect(() => { if (!offscreenCtx || tablesState.length === 0) return; draw(); }, [tablesState, offscreenCtx]); return ( <div style={{ width: "100%", height: "calc(100% - 40px)", overflow: "scroll" }}> <canvas width={1210} height={495} style={{ backgroundColor: "#FFF" }} ref={canvasRef} ></canvas> </div> ); }
额外优化点
- 用
useRef存储isDragging和selectedElement,避免状态更新导致事件重新绑定 - 移除不必要的
canvas和context状态,直接通过canvasRef.current获取 - 确保SVG内容转义正确,避免特殊字符导致加载失败
内容的提问来源于stack exchange,提问作者Boulanfad Mustapha
相关产品推荐
相关产品推荐

