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

React类组件结合requestAnimationFrame()动画报错问题求助

React Canvas动画中requestAnimationFrame导致this指向错误的解决方法

问题描述

使用React类组件结合Canvas创建图案动画时,在componentDidUpdate方法中触发动画,执行一帧后move函数抛出错误:

Uncaught TypeError: Cannot read properties of undefined (reading 'x')

去掉requestAnimationFrame直接循环运行move函数时代码可正常执行,怀疑问题与requestAnimationFrame相关。

问题代码

export default class Canvas extends Component {
    
    constructor(props) {
        super(props);
        this.canvasRef = createRef();
        this.width = parseInt(this.props.width);
        this.height = parseInt(this.props.height);
        this.x = Math.random(this.props.seed) * this.width;
        this.y = Math.random(this.props.seed) * this.height;
        this.r = Math.random(this.props.seed) * 20 + 5;
        this.dx = 10;
        this.dy = 10;
        this.color = "#"+Math.floor(Math.random(this.props.seed) * 1000000);
    }
    
    componentDidMount () {
        console.log("initial view created");
        this.ctx = this.canvasRef.current.getContext('2d');
    }

    componentDidUpdate () {
        console.log("updating view");
        this.move();
    }

    draw () {
        this.ctx.fillStyle = this.color;
        this.ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
        this.ctx.fill();
    }

    
    // 问题出在这个函数
    move() {
        console.log(this.x, this.y);
        this.x += this.dx; // 触发错误的行
        this.y += this.dy;
        this.draw();
        
        requestAnimationFrame(this.move)
    }

    render() {
        return (
            <canvas
                ref={this.canvasRef}
                className={this.props.className}
                width={this.props.width}
                height={this.props.height}
            />
        );
    }
}

原因分析

requestAnimationFrame调用this.move时,函数的this指向丢失,不再指向组件实例,而是默认绑定到全局对象(严格模式下为undefined),因此访问this.x、this.dx等组件属性时会报错。

修复方法

方法1:构造函数中绑定this

在组件构造函数中手动绑定move和draw方法的this指向组件实例:

constructor(props) {
    super(props);
    this.canvasRef = createRef();
    this.width = parseInt(this.props.width);
    this.height = parseInt(this.props.height);
    this.x = Math.random(this.props.seed) * this.width;
    this.y = Math.random(this.props.seed) * this.height;
    this.r = Math.random(this.props.seed) * 20 + 5;
    this.dx = 10;
    this.dy = 10;
    this.color = "#"+Math.floor(Math.random(this.props.seed) * 1000000);
    
    // 绑定this
    this.move = this.move.bind(this);
    this.draw = this.draw.bind(this);
}

方法2:requestAnimationFrame中使用箭头函数

箭头函数会继承当前上下文的this,因此在requestAnimationFrame中用箭头函数包裹this.move调用:

move() {
    console.log(this.x, this.y);
    this.x += this.dx;
    this.y += this.dy;
    this.draw();
    
    requestAnimationFrame(() => this.move());
}

方法3:Class Fields语法定义箭头函数

使用ES6+的class fields语法,直接将move和draw定义为箭头函数,自动绑定组件实例的this:

draw = () => {
    this.ctx.fillStyle = this.color;
    this.ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
    this.ctx.fill();
}

move = () => {
    console.log(this.x, this.y);
    this.x += this.dx;
    this.y += this.dy;
    this.draw();
    
    requestAnimationFrame(this.move);
}

额外优化:避免重复启动动画

componentDidUpdate会在每次组件props/state更新时触发,多次调用this.move()会导致多个动画循环同时运行。建议将动画启动逻辑移到componentDidMount,并在组件卸载时取消动画:

componentDidMount () {
    console.log("initial view created");
    this.ctx = this.canvasRef.current.getContext('2d');
    // 启动动画并保存ID
    this.animationId = requestAnimationFrame(this.move);
}

componentWillUnmount() {
    // 取消动画,避免内存泄漏
    cancelAnimationFrame(this.animationId);
}

// 移除componentDidUpdate中的动画启动代码
// componentDidUpdate () {
//     console.log("updating view");
//     this.move();
// }

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 13:10:33