使用 useEffect 挂钩将 addEventListener 限制为 componentDidMount

Limiting addEventListener to componentDidMount using useEffect hook

我有一个基于 class 的组件,它使用多点触控将子节点添加到 svg,并且效果很好。现在,我正在尝试更新它以使用带钩子的功能组件,如果没有其他原因,只是为了更好地理解它们。

为了停止浏览器对手势使用触摸事件,我需要对它们进行 preventDefault,这要求它们 而不是 passive 并且,因为在我需要使用 svgRef.current.addEventListener('touchstart', handler, {passive: false}) 的合成反应事件中没有暴露被动配置。我在 componentDidMount() 生命周期挂钩中执行此操作,并在 class.

中的 componentWillUnmount() 挂钩中清除它

当我将其转换为带有钩子的功能组件时,我得到以下结果:

export default function Board(props) {

    const [touchPoints, setTouchPoints] = useState([]);
    const svg = useRef();

    useEffect(() => {
        console.log('add touch start');
        svg.current.addEventListener('touchstart', handleTouchStart, { passive: false });

        return () => {
            console.log('remove touch start');
            svg.current.removeEventListener('touchstart', handleTouchStart, { passive: false });
        }
    });

    useEffect(() => {
        console.log('add touch move');
        svg.current.addEventListener('touchmove', handleTouchMove, { passive: false });

        return () => {
            console.log('remove touch move');
            svg.current.removeEventListener('touchmove', handleTouchMove, { passive: false });
        }
    });

    useEffect(() => {
        console.log('add touch end');
        svg.current.addEventListener('touchcancel', handleTouchEnd, { passive: false });
        svg.current.addEventListener('touchend', handleTouchEnd, { passive: false });

        return () => {
            console.log('remove touch end');
            svg.current.removeEventListener('touchend', handleTouchEnd, { passive: false });
            svg.current.removeEventListener('touchcancel', handleTouchEnd, { passive: false });
        }
    });


    const handleTouchStart = useCallback((e) => {
        e.preventDefault();

        // copy the state, mutate it, re-apply it
        const tp = touchPoints.slice();

        // note e.changedTouches is a TouchList not an array
        // so we can't map over it
        for (var i = 0; i < e.changedTouches.length; i++) {
            const touch = e.changedTouches[i];
            tp.push(touch);
        }
        setTouchPoints(tp);
    }, [touchPoints, setTouchPoints]);

    const handleTouchMove = useCallback((e) => {
        e.preventDefault();

        const tp = touchPoints.slice();

        for (var i = 0; i < e.changedTouches.length; i++) {
            const touch = e.changedTouches[i];
            // call helper function to get the Id of the touch
            const index = getTouchIndexById(tp, touch);
            if (index < 0) continue;
            tp[index] = touch;
        }

        setTouchPoints(tp);
    }, [touchPoints, setTouchPoints]);

    const handleTouchEnd = useCallback((e) => {
        e.preventDefault();

        const tp = touchPoints.slice();

        for (var i = 0; i < e.changedTouches.length; i++) {
            const touch = e.changedTouches[i];
            const index = getTouchIndexById(tp, touch);
            tp.splice(index, 1);

        }

        setTouchPoints(tp);
    }, [touchPoints, setTouchPoints]);

    return (
        <svg 
            xmlns={ vars.SVG_NS }
            width={ window.innerWidth }
            height={ window.innerHeight }
        >
            { 
                touchPoints.map(touchpoint =>
                    <TouchCircle 
                        ref={ svg }
                        key={ touchpoint.identifier }
                        cx={ touchpoint.pageX }
                        cy={ touchpoint.pageY }
                        colour={ generateColour() }
                    />
                )
            }
        </svg>
    );
}

这引发的问题是每次渲染更新时,事件侦听器都会被删除并重新添加。这会导致 handleTouchEnd 在它有机会清除添加的触摸和其他异常之前被删除。我还发现触摸事件不起作用,除非我使用手势离开触发更新的浏览器,删除现有的侦听器并添加一组新的侦听器。

我尝试在 useEffect 中使用依赖列表,我看到有几个人引用了 useCallback 和 useRef,但我无法使这项工作变得更好(即删除日志,然后重新添加事件侦听器仍然会在每次更新时全部触发。

有没有办法让 useEffect 只在挂载时触发一次,然后在卸载时清理,或者我应该放弃这个组件的挂钩并坚持使用基于 class 的运行良好的挂钩?

编辑

我也试过将每个事件侦听器移动到它自己的 useEffect 并获得以下控制台日志:

remove touch start
remove touch move
remove touch end
add touch start
add touch move
add touch end

编辑 2

几个人建议添加一个依赖数组,我试过这样:

    useEffect(() => {
        console.log('add touch start');
        svg.current.addEventListener('touchstart', handleTouchStart, { passive: false });

        return () => {
            console.log('remove touch start');
            svg.current.removeEventListener('touchstart', handleTouchStart, { passive: false });
        }
    }, [handleTouchStart]);

    useEffect(() => {
        console.log('add touch move');
        svg.current.addEventListener('touchmove', handleTouchMove, { passive: false });

        return () => {
            console.log('remove touch move');
            svg.current.removeEventListener('touchmove', handleTouchMove, { passive: false });
        }
    }, [handleTouchMove]);

    useEffect(() => {
        console.log('add touch end');
        svg.current.addEventListener('touchcancel', handleTouchEnd, { passive: false });
        svg.current.addEventListener('touchend', handleTouchEnd, { passive: false });

        return () => {
            console.log('remove touch end');
            svg.current.removeEventListener('touchend', handleTouchEnd, { passive: false });
            svg.current.removeEventListener('touchcancel', handleTouchEnd, { passive: false });
        }
    }, [handleTouchEnd]);

但我仍然收到一条日志,说每个 useEffect 都已被删除,然后在每次更新时重新添加(所以每个 touchstarttouchmovetouchend 导致油漆 - 很多 :) )

编辑 3

我已将 window.(add/remove)EventListener 替换为 useRef()

ta

如果您只希望在安装和卸载组件时发生这种情况,则需要为 useEffect 挂钩提供 empty array as the dependency array.

useEffect(() => {
    console.log('adding event listeners');
    window.addEventListener('touchstart', handleTouchStart, { passive: false });
    window.addEventListener('touchend', handleTouchEnd, { passive: false });
    window.addEventListener('touchcancel', handleTouchEnd, { passive: false });
    window.addEventListener('touchmove', handleTouchMove, { passive: false });

    return () => {
        console.log('removing event listeners');
        window.removeEventListener('touchstart', handleTouchStart, { passive: false });
        window.removeEventListener('touchend', handleTouchEnd, { passive: false });
        window.removeEventListener('touchcancel', handleTouchEnd, { passive: false });
        window.removeEventListener('touchmove', handleTouchMove, { passive: false });
    }
}, []);

非常感谢大家 - 我们已经弄清楚了 (w00t)

为了停止多次触发组件 useEffect 挂钩,需要向挂钩提供一个空的依赖数组(如 Son Nguyen and wentjun 所建议),但这意味着当前 touchPoints 处理程序中无法访问状态。

答案(wentjun 建议)在

其中提到了钩子常见问题解答:https://reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often

我的组件就是这样结束的

export default function Board(props) {
    const [touchPoints, setTouchPoints] = useState([]);
    const svg = useRef();

    useEffect(() => {
        // required for the return value
        const svgRef = svg.current;

        const handleTouchStart = (e) => {
            e.preventDefault();

            // use functional version of mutator
            setTouchPoints(tp => {
                // duplicate array
                tp = tp.slice();

                // note e.changedTouches is a TouchList not an array
                // so we can't map over it
                for (var i = 0; i < e.changedTouches.length; i++) {
                    const touch = e.changedTouches[i];
                    const angle = getAngleFromCenter(touch.pageX, touch.pageY);

                    tp.push({ touch, angle });
                }

                return tp;
            });
        };

        const handleTouchMove = (e) => {
            e.preventDefault();

            setTouchPoints(tp => {
                tp = tp.slice();

                // move existing TouchCircle with same key
                for (var i = 0; i < e.changedTouches.length; i++) {
                    const touch = e.changedTouches[i];
                    const index = getTouchIndexById(tp, touch);
                    if (index < 0) continue;
                    tp[index].touch = touch;
                    tp[index].angle = getAngleFromCenter(touch.pageX, touch.pageY);
                }

                return tp;
            });
        };

        const handleTouchEnd = (e) => {
            e.preventDefault();

            setTouchPoints(tp => {
                tp = tp.slice();

                // delete existing TouchCircle with same key
                for (var i = 0; i < e.changedTouches.length; i++) {
                    const touch = e.changedTouches[i];
                    const index = getTouchIndexById(tp, touch);
                    if (index < 0) continue;
                    tp.splice(index, 1);
                }

                return tp;
            });
        };

        console.log('add touch listeners'); // only fires once
        svgRef.addEventListener('touchstart', handleTouchStart, { passive: false });
        svgRef.addEventListener('touchmove', handleTouchMove, { passive: false });
        svgRef.addEventListener('touchcancel', handleTouchEnd, { passive: false });
        svgRef.addEventListener('touchend', handleTouchEnd, { passive: false });

        return () => {
            console.log('remove touch listeners');
            svgRef.removeEventListener('touchstart', handleTouchStart, { passive: false });
            svgRef.removeEventListener('touchmove', handleTouchMove, { passive: false });
            svgRef.removeEventListener('touchend', handleTouchEnd, { passive: false });
            svgRef.removeEventListener('touchcancel', handleTouchEnd, { passive: false });
        }
    }, [setTouchPoints]);

    return (
        <svg 
            ref={ svg }
            xmlns={ vars.SVG_NS }
            width={ window.innerWidth }
            height={ window.innerHeight }
        >
            { 
                touchPoints.map(touchpoint =>
                    <TouchCircle 
                        key={ touchpoint.touch.identifier }
                        cx={ touchpoint.touch.pageX }
                        cy={ touchpoint.touch.pageY }
                        colour={ generateColour() }
                    />
                )
            }
        </svg>
    );
}

注意:我将 setTouchPoints 添加到依赖列表中以更具声明性

Mondo 尊重人

;oB