“那天在document见到你的第一眼,我就下定决心要陪你到天荒地老”
---React
我将从事件从出现到被处理的各个过程来介绍事件机制:
这张图片给我们展示了react事件的各个阶段,我们可以看到有DOM,合成事件层,还有事件处理函数。
我觉得我们如果想要了解事件机制首先要知道的就是事件从注册到执行结束的全过程:
事件注册与触发流程
React 事件的完整生命周期:
- 初始化阶段:
React 在根容器上注册所有支持的事件监听(如onClick
对应click
事件)。 - 触发原生事件:
用户操作触发原生 DOM 事件(如点击按钮)。 - 冒泡到根容器:
事件冒泡到根容器,React 根据event.target
找到实际触发事件的组件。 - 构造合成事件:
创建SyntheticEvent
对象,并触发组件树中对应的事件回调。 - 回调执行:
执行组件中定义的onClick
等处理函数。 - 事件池回收:
事件处理完成后,合成事件对象被释放回事件池。
(在React17以后,移除事件池,合成事件对象不再复用,可直接异步访问事件属性)
事件绑定
React的事件绑定与原生的事件绑定不同,React并不是将click事件绑定到了div的真实DOM上,而是在document处监听了所有的事件,当事件发生并且冒泡到document处的时候,React将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。
并且冒泡到document上的事件也不是原生的浏览器事件;而是react自己合成的合成事件。
所以,如果不想要是事件冒泡的话应该调用event.preventDefault()方法,而不是调用event.stopProppagation()方法。
合成事件
React 将所有原生事件封装为 SyntheticEvent
对象,提供跨浏览器一致性:
- 统一接口:无论浏览器差异(如 IE 与 Chrome),事件对象的行为一致。
- 事件池(Event Pooling):
事件对象会被重用,事件回调执行后,事件属性会被清空。若需异步访问事件属性,需调用e.persist()
保留事件。
handleClick(e) {
e.persist(); // 保留事件对象
setTimeout(() => console.log(e.target), 100);
}
React事件和普通Html事件的区别
区别:
- 对于事件名称命名方式,原生事件为全小写,react 事件采用小驼峰;
- 对于事件函数处理语法,原生事件为字符串,react 事件为函数;
- react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用
preventDefault()
来阻止默认行为。
合成事件是 react 模拟原生 DOM 事件所有能力的一个事件对象,其优点如下:
- 兼容所有浏览器,更好的跨平台;
- 将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。
- 方便 react 统一管理和事务机制。
事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到document 上合成事件才会执行。
事件代理
事件代理(Event Delegation)是一种通过利用事件冒泡机制,将子元素的事件处理委托给父元素统一管理的技术。它在 JavaScript 原生开发和 React 等框架中广泛应用,主要用于优化性能和简化动态元素的事件绑定。
在React底层,主要对合成事件做了两件事:
- 事件委派: React会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。
- 自动绑定: React组件中,每个方法的上下文都会指向该组件的实例,即自动绑定this为当前组件。
一、原生 JavaScript 中的事件代理
1. 核心原理
- 事件冒泡:子元素触发的事件会逐级向上传递到父元素。
- 统一监听:在父元素上绑定事件监听器,通过
event.target
判断实际触发事件的子元素。
2. 代码示例
<ul id="parent">
<li data-id="1">Item 1</li>
<li data-id="2">Item 2</li>
<li data-id="3">Item 3</li>
</ul>
<script>
document.getElementById('parent').addEventListener('click', function(e) {
if (e.target.tagName === 'LI') { // 判断触发元素
const id = e.target.dataset.id;
console.log('Clicked item:', id);
}
});
</script>
3. 优势
- 性能优化:减少事件监听器数量(尤其适用于列表、表格等大量子元素场景)。
- 动态元素支持:新增子元素无需重新绑定事件(如 AJAX 加载的数据)。
4. 缺点
- 事件类型限制:仅适用于冒泡阶段的事件(如
click
,不适用于focus
等无冒泡的事件)。 - 逻辑复杂度:需要手动判断
event.target
,层级较深时可能需递归查找。
二、React 中的事件代理
React 通过合成事件(SyntheticEvent)自动实现了事件代理,开发者无需手动处理。
1. 实现原理
- 统一绑定到根节点:所有事件监听器默认绑定在根容器(如
#root
)。 - 自动委托:React 内部通过事件插件机制,根据事件类型动态管理监听。
2. 代码表现
function List() {
const handleClick = (e) => {
// 直接通过 e.target 获取触发元素(React 已封装跨浏览器兼容)
console.log('Clicked element:', e.target);
};
return (
<ul onClick={handleClick}>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
);
}
3. 优势
- 自动管理:无需手动判断
event.target
,组件销毁时自动解绑事件。 - 性能优化:避免为每个子元素单独绑定监听器,减少内存占用。
- 跨浏览器一致性:合成事件屏蔽了浏览器差异(如
event.preventDefault()
的行为)。
4. 注意事项
- 阻止冒泡:需显式调用
e.stopPropagation()
,否则事件会冒泡到 React 根节点。 - 原生事件冲突:React 事件与原生
addEventListener
混用时,执行顺序可能不符合预期(见下文示例)。
三、事件代理的常见问题与解决方案
1. 事件目标判断错误
- 问题:当子元素内部有嵌套元素时(如
<li><span>Text</span></li>
),e.target
可能指向span
而非li
。 - 解决:递归查找匹配的父元素:
function findClosestParent(el, selector) { while (el && el !== document) { if (el.matches(selector)) return el; el = el.parentNode; } return null; } // React 示例 const handleClick = (e) => { const li = findClosestParent(e.target, 'li'); if (li) console.log('Clicked li:', li.dataset.id); };
2. React 与原生事件混用
- 执行顺序:原生事件监听器先于 React 合成事件执行。
- 示例:
输出顺序:useEffect(() => { document.addEventListener('click', () => { console.log('原生事件'); }); }, []); const handleClick = (e) => { e.stopPropagation(); // 无法阻止原生事件触发 console.log('React 事件'); };
原生事件
→React 事件
解决:在原生事件中调用e.stopImmediatePropagation()
。
四、适用场景
场景 | 原生 JS | React | 说明 |
---|---|---|---|
动态列表/表格 | ✔️ | ✔️ | 新增元素无需重新绑定事件 |
高频事件(如 mousemove ) | ✔️ | ✔️ | 减少监听器数量以优化性能 |
全局事件(如 scroll ) | ✔️ | ❌ | React 建议使用 useEffect 绑定 |
自定义组件通信 | ❌ | ✔️ | 通过父组件统一管理子组件事件 |
end....
“我们这一路走来一直一帆风顺,可惜功亏一篑”
——《花束般的恋爱》