在 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 的值。起初它是“苹果”,然后有一个按钮可以将其更改为“橙色”。但是当点击按钮时真正发生了什么

  1. 在点击按钮之前,SimpleExample 是 运行 作为一个函数,创建一个名为 fruit 的变量并将其初始化为“apple”。
  2. 调用 useEffect 回调,设置 关闭 fruit 变量的间隔。每一秒,“apple”都会打印到控制台。
  3. 现在我们单击按钮。这会调用 setFruit,这将强制 SimpleExample 函数重新 运行。
  4. 创建了一个新的 fruit 变量,这次的值为“orange”。这次 useEffect 没有 被调用,因为没有任何依赖关系发生变化。
  5. 因为 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

在这种情况下,只有一个 canvasRefbubbles 变量,因为它们被定义为引用。因此,效果仅关闭一个实例,并且始终可以访问引用的当前值,即使它发生变化。