MST

星途 面试题库

面试题:React中自定义鼠标交互行为的设计与实现

设计并实现一个React组件,该组件需要模拟一种独特的鼠标交互行为:当鼠标在组件区域内以特定轨迹(如顺时针画圆)移动时,触发一个特定的动画效果。请详细说明实现过程,包括如何检测鼠标轨迹以及如何处理动画触发逻辑等,同时阐述在不同浏览器环境下可能遇到的兼容性问题及解决方案。
21.1万 热度难度
前端开发React

知识考点

AI 面试

面试题答案

一键面试

1. 实现React组件基础结构

首先,创建一个React组件。假设组件名为CircleMouseTrail

import React, { useState, useEffect } from'react';

const CircleMouseTrail = () => {
    const [isDrawingCircle, setIsDrawingCircle] = useState(false);
    const [startX, setStartX] = useState(0);
    const [startY, setStartY] = useState(0);
    const [prevX, setPrevX] = useState(0);
    const [prevY, setPrevY] = useState(0);
    const [radius, setRadius] = useState(0);

    return (
        <div
            style={{
                width: '100vw',
                height: '100vh',
                border: '1px solid black',
                position:'relative'
            }}
            onMouseDown={(e) => {
                setIsDrawingCircle(true);
                setStartX(e.clientX);
                setStartY(e.clientY);
                setPrevX(e.clientX);
                setPrevY(e.clientY);
            }}
            onMouseMove={(e) => {
                if (isDrawingCircle) {
                    // 计算当前点到起始点的距离
                    const dx = e.clientX - startX;
                    const dy = e.clientY - startY;
                    const newRadius = Math.sqrt(dx * dx + dy * dy);
                    setRadius(newRadius);
                    // 这里还需要进一步判断是否是顺时针移动
                    // 例如通过计算角度变化来判断
                    // 简单的通过叉积判断方向(假设顺时针为正方向)
                    const crossProduct = (e.clientX - prevX) * (prevY - startY) - (prevX - startX) * (e.clientY - prevY);
                    if (crossProduct > 0) {
                        // 这里可以添加更多逻辑判断是否真的是在画圆
                        // 例如半径变化不能太大等
                        // 假设已经确定是在顺时针画圆,触发动画
                        // 这里简单设置一个变量表示满足条件,实际可触发动画
                        setIsDrawingCircle(true);
                    } else {
                        setIsDrawingCircle(false);
                    }
                    setPrevX(e.clientX);
                    setPrevY(e.clientY);
                }
            }}
            onMouseUp={() => {
                setIsDrawingCircle(false);
            }}
        >
            {/* 这里可以添加动画相关元素 */}
        </div>
    );
};

export default CircleMouseTrail;

2. 检测鼠标轨迹

  • 起始点记录:在onMouseDown事件中,记录鼠标按下时的坐标startXstartY,作为画圆的起始点。
  • 移动过程记录:在onMouseMove事件中,计算当前鼠标位置与起始点的距离radius,同时记录上一个鼠标位置prevXprevY
  • 判断顺时针方向:通过计算相邻两个向量的叉积来判断鼠标移动方向是否为顺时针。若叉积大于0,则认为是顺时针方向。同时可以添加一些半径变化的限制条件,以确保确实是在画圆而不是随意移动。

3. 处理动画触发逻辑

  • 状态管理:使用isDrawingCircle状态变量来表示当前是否处于符合条件的画圆状态。当检测到鼠标以顺时针画圆时,设置isDrawingCircletrue
  • 触发动画:在实际应用中,可以根据isDrawingCircle的值来触发CSS动画或使用React动画库(如react - springframer - motion等)来实现特定的动画效果。例如,如果使用CSS动画,可以在组件内添加一个元素,并根据isDrawingCircle添加或移除对应的动画类名。
import React, { useState, useEffect } from'react';

const CircleMouseTrail = () => {
    const [isDrawingCircle, setIsDrawingCircle] = useState(false);
    const [startX, setStartX] = useState(0);
    const [startY, setStartY] = useState(0);
    const [prevX, setPrevX] = useState(0);
    const [prevY, setPrevY] = useState(0);
    const [radius, setRadius] = useState(0);

    return (
        <div
            style={{
                width: '100vw',
                height: '100vh',
                border: '1px solid black',
                position:'relative'
            }}
            onMouseDown={(e) => {
                setIsDrawingCircle(true);
                setStartX(e.clientX);
                setStartY(e.clientY);
                setPrevX(e.clientX);
                setPrevY(e.clientY);
            }}
            onMouseMove={(e) => {
                if (isDrawingCircle) {
                    const dx = e.clientX - startX;
                    const dy = e.clientY - startY;
                    const newRadius = Math.sqrt(dx * dx + dy * dy);
                    setRadius(newRadius);
                    const crossProduct = (e.clientX - prevX) * (prevY - startY) - (prevX - startX) * (e.clientY - prevY);
                    if (crossProduct > 0) {
                        setIsDrawingCircle(true);
                    } else {
                        setIsDrawingCircle(false);
                    }
                    setPrevX(e.clientX);
                    setPrevY(e.clientY);
                }
            }}
            onMouseUp={() => {
                setIsDrawingCircle(false);
            }}
        >
            <div
                style={{
                    width: '50px',
                    height: '50px',
                    backgroundColor: 'blue',
                    borderRadius: '50%',
                    position: 'absolute',
                    animation: isDrawingCircle? 'circleAnimation 2s ease - in - out forwards' : 'none'
                }}
            />
            <style>{`
                @keyframes circleAnimation {
                    from {
                        transform: scale(0);
                    }
                    to {
                        transform: scale(1);
                    }
                }
            `}</style>
        </div>
    );
};

export default CircleMouseTrail;

4. 兼容性问题及解决方案

  • 事件兼容性onMouseDownonMouseMoveonMouseUp在大多数现代浏览器中都有较好的支持,但在一些古老浏览器(如IE)中可能存在问题。可以使用addEventListener进行事件绑定的polyfill来解决。例如,使用@types/jquery库来简化事件绑定,或者自己编写兼容代码。
import React, { useState, useEffect } from'react';

const CircleMouseTrail = () => {
    const [isDrawingCircle, setIsDrawingCircle] = useState(false);
    const [startX, setStartX] = useState(0);
    const [startY, setStartY] = useState(0);
    const [prevX, setPrevX] = useState(0);
    const [prevY, setPrevY] = useState(0);
    const [radius, setRadius] = useState(0);

    useEffect(() => {
        const handleMouseDown = (e: MouseEvent) => {
            setIsDrawingCircle(true);
            setStartX(e.clientX);
            setStartY(e.clientY);
            setPrevX(e.clientX);
            setPrevY(e.clientY);
        };
        const handleMouseMove = (e: MouseEvent) => {
            if (isDrawingCircle) {
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                const newRadius = Math.sqrt(dx * dx + dy * dy);
                setRadius(newRadius);
                const crossProduct = (e.clientX - prevX) * (prevY - startY) - (prevX - startX) * (e.clientY - prevY);
                if (crossProduct > 0) {
                    setIsDrawingCircle(true);
                } else {
                    setIsDrawingCircle(false);
                }
                setPrevX(e.clientX);
                setPrevY(e.clientY);
            }
        };
        const handleMouseUp = () => {
            setIsDrawingCircle(false);
        };

        const target = document.querySelector('div');
        if (target) {
            target.addEventListener('mousedown', handleMouseDown);
            target.addEventListener('mousemove', handleMouseMove);
            target.addEventListener('mouseup', handleMouseUp);
        }

        return () => {
            if (target) {
                target.removeEventListener('mousedown', handleMouseDown);
                target.removeEventListener('mousemove', handleMouseMove);
                target.removeEventListener('mouseup', handleMouseUp);
            }
        };
    }, [isDrawingCircle]);

    return (
        <div
            style={{
                width: '100vw',
                height: '100vh',
                border: '1px solid black',
                position:'relative'
            }}
        >
            <div
                style={{
                    width: '50px',
                    height: '50px',
                    backgroundColor: 'blue',
                    borderRadius: '50%',
                    position: 'absolute',
                    animation: isDrawingCircle? 'circleAnimation 2s ease - in - out forwards' : 'none'
                }}
            />
            <style>{`
                @keyframes circleAnimation {
                    from {
                        transform: scale(0);
                    }
                    to {
                        transform: scale(1);
                    }
                }
            `}</style>
        </div>
    );
};

export default CircleMouseTrail;
  • CSS动画兼容性:不同浏览器对CSS动画的支持可能存在差异,尤其是一些旧版本浏览器。可以使用Autoprefixer工具,在构建过程中自动添加浏览器前缀(如-webkit --moz --ms -等),确保动画在各种浏览器中正常显示。例如,在Webpack配置中添加postcss - loaderautoprefixer插件。
module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    'style - loader',
                    {
                        loader: 'css - loader',
                        options: {
                            importLoaders: 1
                        }
                    },
                    {
                        loader: 'postcss - loader',
                        options: {
                            postcssOptions: {
                                plugins: [
                                    require('autoprefixer')
                                ]
                            }
                        }
                    }
                ]
            }
        ]
    }
};