一、背景介绍

在前端开发里,我们常常会用到原生事件和 React 合成事件。原生事件就是浏览器自带的事件,像点击、鼠标移动这些。React 合成事件则是 React 自己封装的事件,它能让我们在不同浏览器里有统一的事件处理体验。不过呢,要是把这两种事件混用,就可能会碰到一些问题,其中一个典型的问题就是 stopPropagation 失效。接下来咱就详细说说这个事儿。

二、原生事件和 React 合成事件基础

2.1 原生事件

原生事件是浏览器直接提供的,我们可以通过 addEventListener 方法来给 DOM 元素添加事件监听器。下面是一个简单的例子:

// HTML 部分
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>原生事件示例</title>
</head>
<body>
    <button id="myButton">点击我</button>
    <script>
        // 获取按钮元素
        const button = document.getElementById('myButton');
        // 给按钮添加点击事件监听器
        button.addEventListener('click', function() {
            console.log('按钮被点击了');
        });
    </script>
</body>
</html>

在这个例子中,我们通过 addEventListener 给按钮添加了一个点击事件监听器。当按钮被点击时,就会在控制台输出 按钮被点击了

2.2 React 合成事件

React 合成事件是 React 封装好的,使用起来和原生事件有点像,但又有区别。下面是一个 React 合成事件的例子:

import React from 'react';
import ReactDOM from 'react-dom/client';

// 定义一个组件
function MyButton() {
    // 定义点击事件处理函数
    const handleClick = () => {
        console.log('React 按钮被点击了');
    };

    return (
        <button onClick={handleClick}>点击 React 按钮</button>
    );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<MyButton />);

在这个例子中,我们在 React 组件里定义了一个 handleClick 函数,然后把它绑定到按钮的 onClick 事件上。当按钮被点击时,就会在控制台输出 React 按钮被点击了

三、stopPropagation 方法的作用

stopPropagation 方法的主要作用是阻止事件冒泡。事件冒泡就是当一个元素上的事件被触发时,这个事件会从这个元素开始,依次向上级元素传播。下面是一个原生事件中使用 stopPropagation 的例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件冒泡和 stopPropagation</title>
</head>
<body>
    <div id="outer">
        <button id="inner">点击我</button>
    </div>
    <script>
        const outer = document.getElementById('outer');
        const inner = document.getElementById('inner');

        // 给外层元素添加点击事件监听器
        outer.addEventListener('click', function() {
            console.log('外层元素被点击了');
        });

        // 给内层元素添加点击事件监听器,并阻止事件冒泡
        inner.addEventListener('click', function(event) {
            event.stopPropagation();
            console.log('内层元素被点击了');
        });
    </script>
</body>
</html>

在这个例子中,当我们点击内层的按钮时,由于使用了 stopPropagation 方法,事件不会冒泡到外层的 div 元素,所以只会在控制台输出 内层元素被点击了

四、原生事件与 React 合成事件混用导致 stopPropagation 失效的原因

4.1 事件处理机制不同

原生事件是直接绑定到 DOM 元素上的,而 React 合成事件是通过事件委托机制,把所有事件都绑定到 document 上。当原生事件和 React 合成事件混用的时候,就可能会出现 stopPropagation 失效的情况。下面是一个混用的例子:

import React from 'react';
import ReactDOM from 'react-dom/client';

// 定义一个组件
function App() {
    const handleReactClick = (event) => {
        event.stopPropagation();
        console.log('React 事件被触发');
    };

    const handleNativeClick = (event) => {
        console.log('原生事件被触发');
    };

    return (
        <div onClick={handleNativeClick}>
            <button onClick={handleReactClick}>点击我</button>
        </div>
    );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

在这个例子中,我们在 div 元素上添加了原生事件监听器(通过 React 的方式),在按钮上添加了 React 合成事件监听器。当我们点击按钮时,虽然在 React 合成事件里调用了 stopPropagation 方法,但原生事件还是会被触发,这是因为 React 合成事件的 stopPropagation 只能阻止 React 事件系统内的冒泡,不能阻止原生事件的冒泡。

4.2 事件执行顺序问题

原生事件和 React 合成事件的执行顺序也会影响 stopPropagation 的效果。在大多数浏览器中,原生事件会先于 React 合成事件执行。这就导致即使在 React 合成事件里调用了 stopPropagation,也无法阻止之前已经触发的原生事件的传播。下面是一个更复杂的例子:

import React from 'react';
import ReactDOM from 'react-dom/client';

// 定义一个组件
function ComplexApp() {
    const handleReactClick = (event) => {
        event.stopPropagation();
        console.log('React 事件被触发,尝试阻止冒泡');
    };

    return (
        <div>
            <button onClick={handleReactClick}>点击我</button>
            <script>
                const div = document.querySelector('div');
                div.addEventListener('click', function() {
                    console.log('原生事件被触发');
                });
            </script>
        </div>
    );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<ComplexApp />);

在这个例子中,当我们点击按钮时,原生事件会先触发,然后才是 React 合成事件。即使在 React 合成事件里调用了 stopPropagation,也无法阻止原生事件的触发。

五、真实触发过程分析

5.1 事件触发顺序

当我们点击一个元素时,事件的触发顺序是这样的:首先是原生事件的捕获阶段,然后是原生事件的目标阶段,接着是原生事件的冒泡阶段,最后才是 React 合成事件的触发。下面是一个详细的例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件触发顺序</title>
    <script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
    <div id="root"></div>
    <script type="text/babel">
        function App() {
            const handleReactClick = (event) => {
                console.log('React 合成事件被触发');
            };

            return (
                <div onClick={handleReactClick}>
                    <button>点击我</button>
                </div>
            );
        }

        const root = ReactDOM.createRoot(document.getElementById('root'));
        root.render(<App />);

        const div = document.querySelector('div');
        const button = document.querySelector('button');

        // 原生事件捕获阶段
        div.addEventListener('click', function() {
            console.log('原生事件捕获阶段:div 被触发');
        }, true);

        button.addEventListener('click', function() {
            console.log('原生事件捕获阶段:button 被触发');
        }, true);

        // 原生事件冒泡阶段
        div.addEventListener('click', function() {
            console.log('原生事件冒泡阶段:div 被触发');
        }, false);

        button.addEventListener('click', function() {
            console.log('原生事件冒泡阶段:button 被触发');
        }, false);
    </script>
</body>
</html>

在这个例子中,当我们点击按钮时,控制台的输出顺序是:

原生事件捕获阶段:div 被触发
原生事件捕获阶段:button 被触发
原生事件冒泡阶段:button 被触发
原生事件冒泡阶段:div 被触发
React 合成事件被触发

5.2 stopPropagation 失效的具体表现

从上面的例子可以看出,即使在 React 合成事件里调用了 stopPropagation,也无法阻止原生事件的冒泡。因为 React 合成事件是在原生事件冒泡之后才触发的,所以当调用 stopPropagation 时,原生事件的冒泡过程已经完成了。

六、应用场景

6.1 表单验证

在表单验证中,我们可能会同时使用原生事件和 React 合成事件。比如,当用户输入内容时,我们可以使用原生的 input 事件来实时验证输入的内容,同时使用 React 合成事件来提交表单。如果不小心混用了这两种事件,就可能会出现 stopPropagation 失效的问题。下面是一个简单的表单验证例子:

import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';

function Form() {
    const [inputValue, setInputValue] = useState('');

    const handleInput = (event) => {
        setInputValue(event.target.value);
        // 这里可以进行实时验证
        console.log('输入内容:', event.target.value);
    };

    const handleSubmit = (event) => {
        event.preventDefault();
        event.stopPropagation();
        console.log('表单提交,输入内容:', inputValue);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input type="text" value={inputValue} onChange={handleInput} />
            <button type="submit">提交</button>
        </form>
    );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Form />);

在这个例子中,如果在表单外面还有一个元素添加了原生的点击事件监听器,当我们点击提交按钮时,可能会出现原生事件也被触发的情况,即使在 React 合成事件里调用了 stopPropagation

6.2 模态框关闭

在模态框关闭的场景中,我们可能会使用原生事件来处理模态框外部的点击事件,使用 React 合成事件来处理模态框内部的关闭按钮点击事件。如果混用这两种事件,也可能会出现 stopPropagation 失效的问题。下面是一个模态框的例子:

import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';

function Modal() {
    const [isOpen, setIsOpen] = useState(true);

    const handleClose = (event) => {
        event.stopPropagation();
        setIsOpen(false);
        console.log('模态框关闭');
    };

    const handleOutsideClick = () => {
        if (isOpen) {
            setIsOpen(false);
            console.log('点击模态框外部,模态框关闭');
        }
    };

    return (
        <div onClick={handleOutsideClick} style={{ display: isOpen ? 'block' : 'none' }}>
            <div style={{ background: 'white', padding: '20px' }}>
                <h2>模态框标题</h2>
                <p>模态框内容</p>
                <button onClick={handleClose}>关闭</button>
            </div>
        </div>
    );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Modal />);

在这个例子中,如果在点击关闭按钮时,stopPropagation 失效,就可能会同时触发模态框外部的点击事件,导致模态框意外关闭。

七、技术优缺点

7.1 优点

  • 混用原生事件和 React 合成事件可以让我们利用两者的优势。原生事件可以直接操作 DOM,有些复杂的 DOM 操作使用原生事件更方便。React 合成事件则提供了统一的事件处理接口,方便在不同浏览器中使用。
  • 例如,在响应用户的滚动事件时,原生事件可以更精确地获取滚动的位置,而 React 合成事件可以方便地在组件间传递滚动信息。
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom/client';

function ScrollComponent() {
    useEffect(() => {
        const handleScroll = () => {
            const scrollTop = window.scrollY;
            console.log('滚动位置:', scrollTop);
        };

        window.addEventListener('scroll', handleScroll);

        return () => {
            window.removeEventListener('scroll', handleScroll);
        };
    }, []);

    return (
        <div style={{ height: '2000px' }}>
            <h1>滚动页面</h1>
        </div>
    );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<ScrollComponent />);

7.2 缺点

  • 最大的缺点就是可能会导致 stopPropagation 失效,从而引发一些难以调试的问题。如前面的例子所示,当原生事件和 React 合成事件混用,且事件冒泡过程复杂时,stopPropagation 无法按预期阻止事件传播。
  • 另外,混用会增加代码的复杂度,使代码的维护和理解变得困难。不同的事件处理机制和执行顺序需要开发者仔细考虑,容易出现错误。

八、注意事项

8.1 尽量避免混用

在开发过程中,尽量避免同时使用原生事件和 React 合成事件。如果能使用 React 合成事件实现的功能,就尽量使用它,因为 React 合成事件有更好的兼容性和一致性。

8.2 明确事件执行顺序

如果确实需要混用,一定要明确原生事件和 React 合成事件的执行顺序,避免因为顺序问题导致 stopPropagation 失效。

8.3 测试和调试

在使用原生事件和 React 合成事件混用时,要进行充分的测试和调试,确保事件的执行符合预期。可以使用浏览器的开发者工具查看事件的触发情况,及时发现和解决问题。

九、文章总结

在前端开发里,原生事件和 React 合成事件都有各自的用途。但当我们把它们混用的时候,就可能会碰到 stopPropagation 失效的问题。这主要是因为两者的事件处理机制不同,以及事件执行顺序的影响。我们通过多个例子详细分析了 stopPropagation 失效的真实触发过程,也介绍了一些应用场景、技术优缺点和注意事项。在开发过程中,我们要尽量避免混用这两种事件,如果必须混用,就要明确事件的执行顺序,并且做好测试和调试工作,这样才能避免出现不必要的问题,提高代码的稳定性和可维护性。