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
相关产品推荐
相关产品推荐

