You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中useEffect无法更新two.js绘图应用线条宽度的问题

问题分析
  • 闭包变量捕获失效:val是普通变量,drag函数在组件初始化时创建,闭包捕获的是初始状态的val(未定义),后续useEffect更新val后,drag函数仍读取旧值。
  • Two.js实例重复初始化:var two = new Two(...)直接写在组件顶层,每次组件渲染都会生成新的Two实例,导致绘图上下文混乱。
  • 线条宽度更新时机错误:useEffect中判断line是否存在,但line是鼠标按下后才创建的,初始状态下line为null,导致useEffect首次执行时不会更新val。
  • 异步状态日志误导:handleDraw里的console.log(draw)打印的是更新前的旧值,因为setState是异步操作。
修复方案

1. 用React状态管理线条宽度

将线条宽度改为React状态,避免闭包捕获旧值的问题,同时支持实时更新。

2. 正确初始化Two.js实例

将Two实例的创建放入useEffect,并用useRef保存实例引用,确保只初始化一次。

3. 优化事件绑定与变量存储

用useRef存储临时变量(如line、mouse),将鼠标事件绑定到useEffect中,依赖线条宽度状态实现实时更新。

完整修复代码

import React, { useRef, useEffect, useState } from 'react'
import { Link } from "react-router-dom";
import Two from "two.js";
import './paintApp.css';

function PaintApp() {
  const ref = useRef(null);
  const twoRef = useRef(null); // 保存Two实例
  const [draw, setDraw] = useState(false);
  const [lineWidth, setLineWidth] = useState(10); // 用状态管理线条宽度

  // 初始化Two.js实例
  useEffect(() => {
    if (!twoRef.current) {
      const two = new Two({
        type: Two.Types.svg,
        width: 900,
        height: 500,
        autostart: true
      });
      two.appendTo(ref.current);
      twoRef.current = two;
    }
  }, []);

  // 切换绘图/擦除模式时更新线条宽度
  useEffect(() => {
    const newWidth = draw ? 0 : 10;
    setLineWidth(newWidth);
    
    // 更新已有线条的宽度
    if (twoRef.current) {
      twoRef.current.scene.children.forEach(child => {
        if (child.linewidth !== undefined) {
          child.linewidth = newWidth;
        }
      });
      twoRef.current.update();
    }
  }, [draw]);

  const handleDraw = () => {
    setDraw(prev => !prev);
  };

  // 用ref存储临时变量,避免闭包问题
  const lineRef = useRef(null);
  const mouseRef = useRef(new Two.Vector());

  // 鼠标/触摸事件绑定到useEffect
  useEffect(() => {
    const two = twoRef.current;
    if (!two) return;

    const handleMouseDown = (e) => {
      mouseRef.current.set(e.offsetX, e.offsetY);
      lineRef.current = null;
      window.addEventListener('mousemove', handleDrag);
      window.addEventListener('mouseup', handleDragEnd);
    };

    const handleDrag = (e) => {
      const x = e.offsetX;
      const y = e.offsetY;
      if (!lineRef.current) {
        const v1 = makePoint(mouseRef.current);
        const v2 = makePoint(x, y);
        lineRef.current = two.makeCurve([v1, v2], true);
        lineRef.current.noFill().stroke = '#333';
        // 直接读取最新的线条宽度状态
        lineRef.current.linewidth = lineWidth;

        lineRef.current.vertices.forEach(v => {
          v.addSelf(lineRef.current.translation);
        });
        lineRef.current.translation.clear();
      } else {
        const v1 = makePoint(x, y);
        lineRef.current.vertices.push(v1);
      }
      mouseRef.current.set(x, y);
    };

    const handleDragEnd = () => {
      window.removeEventListener('mousemove', handleDrag);
      window.removeEventListener('mouseup', handleDragEnd);
    };

    const handleTouchStart = (e) => {
      e.preventDefault();
      const touch = e.changedTouches[0];
      mouseRef.current.set(touch.pageX, touch.pageY);
      lineRef.current = null;
      window.addEventListener('touchmove', handleTouchDrag);
      window.addEventListener('touchend', handleTouchEnd);
    };

    const handleTouchDrag = (e) => {
      e.preventDefault();
      const touch = e.changedTouches[0];
      handleDrag({
        offsetX: touch.pageX,
        offsetY: touch.pageY
      });
    };

    const handleTouchEnd = (e) => {
      e.preventDefault();
      window.removeEventListener('touchmove', handleTouchDrag);
      window.removeEventListener('touchend', handleTouchEnd);
    };

    window.addEventListener('mousedown', handleMouseDown);
    window.addEventListener('touchstart', handleTouchStart);

    // 组件卸载时清理事件监听
    return () => {
      window.removeEventListener('mousedown', handleMouseDown);
      window.removeEventListener('touchstart', handleTouchStart);
      window.removeEventListener('mousemove', handleDrag);
      window.removeEventListener('mouseup', handleDragEnd);
      window.removeEventListener('touchmove', handleTouchDrag);
      window.removeEventListener('touchend', handleTouchEnd);
    };
  }, [lineWidth]);

  twoRef.current?.bind('update', update);

  function update(frameCount, timeDelta) {
    const two = twoRef.current;
    if (!two) return;
    for (let i = 0; i < two.scene.children.length; i++) {
      const child = two.scene.children[i];
      for (let j = 0; j < child.vertices.length; j++) {
        const v = child.vertices[j];
        if (!v.position) return;
        v.x = v.position.x;
        v.y = v.position.y;
      }
    }
  }

  function makePoint(x, y) {
    if (arguments.length <= 1) {
      y = x.y;
      x = x.x;
    }
    const v = new Two.Anchor(x, y);
    v.position = new Two.Vector().copy(v);
    return v;
  }

  return (
    <div>
      <div>paintApp</div>
      <div><Link to='/application'>Back</Link></div>
      <button onClick={handleDraw}>{draw ? '切换到绘图' : '切换到擦除'}</button>
      <div ref={ref} id="picture"></div>
    </div>
  )
}

export default PaintApp

关键修复说明

  • useRef存储实例与临时变量:避免组件渲染时重复创建Two实例,同时解决闭包中变量捕获旧值的问题。
  • 状态驱动线条宽度:用lineWidth状态管理宽度值,确保drag函数能读取到最新值。
  • 事件绑定依赖状态:鼠标事件绑定依赖lineWidth,宽度变化时重新绑定事件,保证闭包捕获最新状态。
  • 批量更新已有线条:切换模式时同步更新画布上所有已绘制线条的宽度,实现模式切换的即时生效。

内容的提问来源于stack exchange,提问作者RadhoFan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 18:05:39