一、背景介绍
在前端开发里,我们常常会用到原生事件和 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 失效的真实触发过程,也介绍了一些应用场景、技术优缺点和注意事项。在开发过程中,我们要尽量避免混用这两种事件,如果必须混用,就要明确事件的执行顺序,并且做好测试和调试工作,这样才能避免出现不必要的问题,提高代码的稳定性和可维护性。
评论
围绕“原生事件与React合成事件混用风险:stopPropagation失效的真实触发过程”参与讨论