在 useEffect 中访问和更新 canvas 节点回调 - React
Accessing and updating canvas node callback inside of useEffect - React
我创建了一个 canvas,它使用回调设置为一个状态。然后根据鼠标 x 和 y 创建圆圈,然后在清除每个帧的 canvas 后绘制到 canvas 状态。每个绘制框都会减小圆圈的半径,直到它们消失
目前 canvas 仅在 canvasDraw() 内部更新鼠标移动。
问题:canvas 需要使用 useEffect() 方法的中间间隔进行更新,以便圆圈的半径随时间逐渐减小。
出于某种原因检索 useEffect() 内部的 canvas 状态 returns null。我认为这可能是因为 useEffect 间隔被调用一次但在 ctx 能够被初始化为一个状态之前。但是我不知道从这里去哪里...
以下 link 有助于修复我的代码中的一些漏洞,但在 useEffect() 处仍会导致空状态:
Is it safe to use ref.current as useEffect's dependency when ref points to a DOM element?
import React, { useEffect, useState, useCallback, useReducer } from "react";
const Canvas = () => {
const [isMounted, toggle] = useReducer((p) => !p, true);
const [canvasRef, setCanvasRef] = useState();
const [ctx, setCtx] = useState();
const handleCanvas = useCallback((node) => {
setCanvasRef(node);
setCtx(node?.getContext("2d"));
}, []);
const [xy, setxy] = useState(0);
const [canDraw, setCanDraw] = useState(false);
const [bubbles, setBubbles] = useState([]);
const canvasDraw = (e) => {
setxy(e.nativeEvent.offsetX * e.nativeEvent.offsetY);
xy % 10 == 0 ? setCanDraw(true): setCanDraw(false);
canDraw && createBubble(e.nativeEvent.offsetX, e.nativeEvent.offsetY);
drawBubbles();
};
const createBubble = (x, y) => {
const bubble = {
x: x,
y: y,
radius: 10 + (Math.random() * (100 - 10))
};
setBubbles(bubbles => [...bubbles, bubble])
}
const drawBubbles = useCallback(() => {
if (ctx != null){
ctx.clearRect(0,0,canvasRef.width,canvasRef.height);
bubbles.forEach((bubble) => {
bubble.radius = Math.max(0, bubble.radius - (0.01 * bubble.radius));
ctx.beginPath()
ctx.arc(bubble.x, bubble.y, bubble.radius, 0, 2 * Math.PI, false)
ctx.fillStyle = "#B4E4FF"
ctx.fill()
}, [])
}
});
useEffect(() => {
const interval = setInterval(() => {
console.log(ctx);
drawBubbles(ctx); // <------ THE PROBLEM (ctx is null)
}, 100)
return () => clearInterval(interval);
}, []);
return (
<main className="pagecontainer">
<section className="page">
<h1>Bubbles!</h1>
{isMounted && <canvas
onMouseMove={canvasDraw}
ref={handleCanvas}
width={`1280px`}
height={`720px`}
/>}
</section>
</main>
);
}
export default Canvas
我从 useEffect 和 useState 中学到的一个艰难的方法是,每次组件重新呈现时,您必须非常小心实际 closed over 的变量。
举个例子更容易理解:
export const SimpleExample = () => {
const [ fruit, setFruit ] = useState('apple')
useEffect(() => {
const interval = setInterval(() => {
console.log(fruit)
}, 1000)
return () => clearInterval(interval)
}, [])
return (<div>
<p>{fruit}</p>
<button onClick={() => setFruit('orange')}>Make Orange</button>
<p>The console will continue to print "apple".</p>
</div>)
}
在这里,我们每秒打印 fruit
的值。起初它是“苹果”,然后有一个按钮可以将其更改为“橙色”。但是当点击按钮时真正发生了什么?
- 在点击按钮之前,
SimpleExample
是 运行 作为一个函数,创建一个名为 fruit
的变量并将其初始化为“apple”。
- 调用
useEffect
回调,设置 关闭 fruit
变量的间隔。每一秒,“apple”都会打印到控制台。
- 现在我们单击按钮。这会调用
setFruit
,这将强制 SimpleExample
函数重新 运行。
- 创建了一个新的
fruit
变量,这次的值为“orange”。这次 useEffect 没有 被调用,因为没有任何依赖关系发生变化。
- 因为 useEffect 在创建的 first
fruit
变量上关闭,所以它会继续打印。它打印“苹果”,而不是“橙色”。
您的代码中发生了同样的事情,但使用了 ctx
变量。当 ctx
变量为 null 时,效果已关闭,所以它只记得这些。
那么我们该如何解决这个问题?
您应该做什么取决于用例。例如,您可以通过提供对 ctx
的依赖来重新调用效果。事实证明,这是解决我们的 fruit
示例的好方法。
useEffect(() => {
const interval = setInterval(() => {
console.log(fruit)
}, 1000)
return () => clearInterval(interval)
}, [fruit])
然而,在您的特定情况下,我们正在处理 canvas。我不是 100% 确定 canvas 和 React 的最佳实践是什么,但我的直觉告诉你不一定要在每次添加气泡时重新渲染 canvas(回想一下:任何时候调用 set*
函数都会重新渲染)。相反,您想 重绘 canvas。 Refs 恰好可用于保存变量而不触发重新渲染,让您完全控制发生的事情。
import React, { useEffect, useRef } from "react";
const Canvas = () => {
const canvasRef = useRef()
const bubbles = useRef([])
const createBubble = (x, y) => {
const bubble = {
x: x,
y: y,
radius: 10 + (Math.random() * (100 - 10))
};
bubbles.current = [...bubbles.current, bubble]
}
const drawBubbles = () => {
const ctx = canvasRef.current?.getContext('2d')
if (ctx != null) {
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
bubbles.current.forEach((bubble) => {
bubble.radius = Math.max(0, bubble.radius - (0.01 * bubble.radius));
ctx.beginPath()
ctx.arc(bubble.x, bubble.y, bubble.radius, 0, 2 * Math.PI, false)
ctx.fillStyle = "#B4E4FF"
ctx.fill()
})
}
};
const canvasDraw = (e) => {
const canDraw = (e.nativeEvent.offsetX * e.nativeEvent.offsetY) % 10 == 0
if (canDraw) createBubble(e.nativeEvent.offsetX, e.nativeEvent.offsetY);
drawBubbles();
};
useEffect(() => {
const interval = setInterval(() => {
drawBubbles(canvasRef.current?.getContext('2d'));
}, 100)
return () => clearInterval(interval);
}, []);
return (
<main className="pagecontainer">
<section className="page">
<h1>Bubbles!</h1>
<canvas
onMouseMove={canvasDraw}
ref={canvasRef}
width={`1280px`}
height={`720px`}
/>
</section>
</main>
);
}
export default Canvas
在这种情况下,只有一个 canvasRef
和 bubbles
变量,因为它们被定义为引用。因此,效果仅关闭一个实例,并且始终可以访问引用的当前值,即使它发生变化。
我创建了一个 canvas,它使用回调设置为一个状态。然后根据鼠标 x 和 y 创建圆圈,然后在清除每个帧的 canvas 后绘制到 canvas 状态。每个绘制框都会减小圆圈的半径,直到它们消失
目前 canvas 仅在 canvasDraw() 内部更新鼠标移动。
问题:canvas 需要使用 useEffect() 方法的中间间隔进行更新,以便圆圈的半径随时间逐渐减小。
出于某种原因检索 useEffect() 内部的 canvas 状态 returns null。我认为这可能是因为 useEffect 间隔被调用一次但在 ctx 能够被初始化为一个状态之前。但是我不知道从这里去哪里...
以下 link 有助于修复我的代码中的一些漏洞,但在 useEffect() 处仍会导致空状态: Is it safe to use ref.current as useEffect's dependency when ref points to a DOM element?
import React, { useEffect, useState, useCallback, useReducer } from "react";
const Canvas = () => {
const [isMounted, toggle] = useReducer((p) => !p, true);
const [canvasRef, setCanvasRef] = useState();
const [ctx, setCtx] = useState();
const handleCanvas = useCallback((node) => {
setCanvasRef(node);
setCtx(node?.getContext("2d"));
}, []);
const [xy, setxy] = useState(0);
const [canDraw, setCanDraw] = useState(false);
const [bubbles, setBubbles] = useState([]);
const canvasDraw = (e) => {
setxy(e.nativeEvent.offsetX * e.nativeEvent.offsetY);
xy % 10 == 0 ? setCanDraw(true): setCanDraw(false);
canDraw && createBubble(e.nativeEvent.offsetX, e.nativeEvent.offsetY);
drawBubbles();
};
const createBubble = (x, y) => {
const bubble = {
x: x,
y: y,
radius: 10 + (Math.random() * (100 - 10))
};
setBubbles(bubbles => [...bubbles, bubble])
}
const drawBubbles = useCallback(() => {
if (ctx != null){
ctx.clearRect(0,0,canvasRef.width,canvasRef.height);
bubbles.forEach((bubble) => {
bubble.radius = Math.max(0, bubble.radius - (0.01 * bubble.radius));
ctx.beginPath()
ctx.arc(bubble.x, bubble.y, bubble.radius, 0, 2 * Math.PI, false)
ctx.fillStyle = "#B4E4FF"
ctx.fill()
}, [])
}
});
useEffect(() => {
const interval = setInterval(() => {
console.log(ctx);
drawBubbles(ctx); // <------ THE PROBLEM (ctx is null)
}, 100)
return () => clearInterval(interval);
}, []);
return (
<main className="pagecontainer">
<section className="page">
<h1>Bubbles!</h1>
{isMounted && <canvas
onMouseMove={canvasDraw}
ref={handleCanvas}
width={`1280px`}
height={`720px`}
/>}
</section>
</main>
);
}
export default Canvas
我从 useEffect 和 useState 中学到的一个艰难的方法是,每次组件重新呈现时,您必须非常小心实际 closed over 的变量。
举个例子更容易理解:
export const SimpleExample = () => {
const [ fruit, setFruit ] = useState('apple')
useEffect(() => {
const interval = setInterval(() => {
console.log(fruit)
}, 1000)
return () => clearInterval(interval)
}, [])
return (<div>
<p>{fruit}</p>
<button onClick={() => setFruit('orange')}>Make Orange</button>
<p>The console will continue to print "apple".</p>
</div>)
}
在这里,我们每秒打印 fruit
的值。起初它是“苹果”,然后有一个按钮可以将其更改为“橙色”。但是当点击按钮时真正发生了什么?
- 在点击按钮之前,
SimpleExample
是 运行 作为一个函数,创建一个名为fruit
的变量并将其初始化为“apple”。 - 调用
useEffect
回调,设置 关闭fruit
变量的间隔。每一秒,“apple”都会打印到控制台。 - 现在我们单击按钮。这会调用
setFruit
,这将强制SimpleExample
函数重新 运行。 - 创建了一个新的
fruit
变量,这次的值为“orange”。这次 useEffect 没有 被调用,因为没有任何依赖关系发生变化。 - 因为 useEffect 在创建的 first
fruit
变量上关闭,所以它会继续打印。它打印“苹果”,而不是“橙色”。
您的代码中发生了同样的事情,但使用了 ctx
变量。当 ctx
变量为 null 时,效果已关闭,所以它只记得这些。
那么我们该如何解决这个问题?
您应该做什么取决于用例。例如,您可以通过提供对 ctx
的依赖来重新调用效果。事实证明,这是解决我们的 fruit
示例的好方法。
useEffect(() => {
const interval = setInterval(() => {
console.log(fruit)
}, 1000)
return () => clearInterval(interval)
}, [fruit])
然而,在您的特定情况下,我们正在处理 canvas。我不是 100% 确定 canvas 和 React 的最佳实践是什么,但我的直觉告诉你不一定要在每次添加气泡时重新渲染 canvas(回想一下:任何时候调用 set*
函数都会重新渲染)。相反,您想 重绘 canvas。 Refs 恰好可用于保存变量而不触发重新渲染,让您完全控制发生的事情。
import React, { useEffect, useRef } from "react";
const Canvas = () => {
const canvasRef = useRef()
const bubbles = useRef([])
const createBubble = (x, y) => {
const bubble = {
x: x,
y: y,
radius: 10 + (Math.random() * (100 - 10))
};
bubbles.current = [...bubbles.current, bubble]
}
const drawBubbles = () => {
const ctx = canvasRef.current?.getContext('2d')
if (ctx != null) {
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
bubbles.current.forEach((bubble) => {
bubble.radius = Math.max(0, bubble.radius - (0.01 * bubble.radius));
ctx.beginPath()
ctx.arc(bubble.x, bubble.y, bubble.radius, 0, 2 * Math.PI, false)
ctx.fillStyle = "#B4E4FF"
ctx.fill()
})
}
};
const canvasDraw = (e) => {
const canDraw = (e.nativeEvent.offsetX * e.nativeEvent.offsetY) % 10 == 0
if (canDraw) createBubble(e.nativeEvent.offsetX, e.nativeEvent.offsetY);
drawBubbles();
};
useEffect(() => {
const interval = setInterval(() => {
drawBubbles(canvasRef.current?.getContext('2d'));
}, 100)
return () => clearInterval(interval);
}, []);
return (
<main className="pagecontainer">
<section className="page">
<h1>Bubbles!</h1>
<canvas
onMouseMove={canvasDraw}
ref={canvasRef}
width={`1280px`}
height={`720px`}
/>
</section>
</main>
);
}
export default Canvas
在这种情况下,只有一个 canvasRef
和 bubbles
变量,因为它们被定义为引用。因此,效果仅关闭一个实例,并且始终可以访问引用的当前值,即使它发生变化。