从 JS 到 React:一文吃透事件机制的 “前世今生”

101 阅读5分钟

一、JS 事件机制:浏览器如何 “听懂” 你的点击?

(一)事件流的三个阶段:捕获、目标、冒泡

浏览器渲染的页面就像一棵 DOM 树,当你点击一个按钮时,事件会经历一场 “奇幻旅程”:

  1. 捕获阶段(从上往下找目标)
    事件从document出发,沿着 DOM 树层层向下,就像家长在客厅喊孩子吃饭,先问爷爷、再问爸爸,最后找到你。

    parent.addEventListener('click', handler, true); // 第三个参数true开启捕获阶段监听
    
  2. 目标阶段(找到真正触发事件的元素)
    终于到达你点击的那个按钮(event.target),此时捕获和冒泡阶段的监听都会触发(但顺序不同哦)。

  3. 冒泡阶段(从下往上找家长)
    事件从目标元素开始反向冒泡,就像你吃完饭告诉爸爸,爸爸再告诉爷爷。这是最常用的阶段,因为addEventListener默认useCapture=false

    child.addEventListener('click', handler); // 省略第三个参数,默认冒泡阶段
    

(二)事件监听的正确姿势:从 DOM0 到 DOM2

  • DOM0 时代(简单但单一)
    直接通过onclick属性绑定,一个事件只能绑定一个处理函数,后绑定的会覆盖前一个。

    <button onclick="handleClick()">点击我</button>
    
  • DOM2 时代(灵活又强大)
    addEventListener支持同一事件绑定多个处理函数,还能控制捕获 / 冒泡阶段。

    button.addEventListener('click', handler1);
    button.addEventListener('click', handler2); // 两个处理函数都会执行
    

(三)异步处理:事件回调的 “非阻塞” 特性

当事件触发时,回调函数不会立即执行,而是加入事件队列,等主线程空闲后再执行。这就是为什么点击事件里的setTimeout会延迟执行 —— 它们都是异步任务。

button.addEventListener('click', () => {
  console.log('立即执行'); // 主线程直接执行
  setTimeout(() => {
    console.log('2秒后执行'); // 进入事件队列等待
  }, 2000);
});

二、事件委托:用 “家长管理法” 优化事件监听

(一)为什么需要事件委托?

假设你有 1000 个列表项,每个都要绑定点击事件,直接绑定会创建 1000 个监听器,像雇了 1000 个保安,太浪费!事件委托就是让 “家长”(父元素)统一管理:

  • 性能优化:只需在父元素绑定一个监听器,内存占用直线下降。
  • 动态节点友好:新增的子元素自动继承事件处理,无需重复绑定。
  • 代码简洁:告别重复代码,逻辑集中在一个地方。

(二)核心原理:靠event.target找到 “真凶”

事件冒泡时,父元素的监听器通过event.target判断实际点击的子元素。比如点击列表项:

ul.addEventListener('click', (event) => {
  if (event.target.tagName === 'LI') { // 只处理li元素的点击
    console.log('点击了列表项:', event.target.textContent);
  }
});

(三)实战案例:点击空白处关闭菜单

很多场景需要点击页面空白处关闭弹窗,但弹窗内部点击要阻止冒泡。事件委托轻松搞定:

// 绑定全局点击事件
document.addEventListener('click', (event) => {
  if (!menu.contains(event.target)) { // 如果点击的不是menu内部元素
    menu.style.display = 'none';
  }
});

// 弹窗内部的按钮阻止冒泡,避免触发全局关闭
closeButton.addEventListener('click', (event) => {
  event.stopPropagation(); // 阻断冒泡,不让事件“往上跑”
  // 其他处理逻辑
});

三、React 事件机制:框架如何 “魔改” 原生事件?

(一)合成事件(SyntheticEvent):跨浏览器的 “统一翻译官”

React 没有直接使用原生事件,而是自己封装了一个 “翻译官”—— 合成事件:

  • 统一挂载点:所有事件都委托到根节点#root(React 17 前是document),就像把所有语言的文件交给一个翻译公司处理。
  • 事件池技术:重复利用事件对象,避免频繁创建销毁,大型应用性能提升显著。以前每次点击都要新建一个事件对象,现在可以循环使用,环保又高效!
  • 跨浏览器兼容:自动处理event.target和 IE 的event.srcElement差异,开发者再也不用写兼容代码啦~

(二)合成事件 vs 原生事件:这些区别要牢记

特性合成事件原生事件
绑定方式JSX 中驼峰命名(onClickaddEventListener或 HTML 属性(onclick
事件对象SyntheticEvent(封装原生对象)浏览器原生Event
内存管理自动绑定 / 解绑,无需手动处理需手动调用removeEventListener
异步访问需调用e.persist()防止回收可直接使用

(三)React 事件处理的 “坑” 与对策

  1. 异步中访问事件对象
    合成事件对象会被回收,异步代码中需要e.persist()“留住” 它:

    const handleClick = (e) => {
      e.persist(); // 阻止事件对象被回收
      setTimeout(() => {
        console.log('异步访问target:', e.target); // 现在可以正常访问啦~
      }, 1000);
    };
    
  2. 阻止冒泡的正确姿势
    e.stopPropagation()只能阻止合成事件冒泡,若要同时影响原生事件,需操作e.nativeEvent

    e.nativeEvent.stopImmediatePropagation(); // 彻底阻断所有事件传播
    

四、代码实战:从原生到 React 的事件委托对比

(一)原生 JS 事件委托示例:动态列表点击

<ul id="myList">
  <li data-id="1">Item 1</li>
  <li data-id="2">Item 2</li>
</ul>
<button id="addItem">添加新项</button>

<script>
  const list = document.getElementById('myList');
  list.addEventListener('click', (event) => {
    if (event.target.matches('li')) { // 匹配li元素
      console.log('点击了Item:', event.target.dataset.id);
    }
  });

  document.getElementById('addItem').addEventListener('click', () => {
    const newLi = document.createElement('li');
    newLi.dataset.id = Date.now();
    newLi.textContent = `New Item ${Date.now()}`;
    list.appendChild(newLi); // 新增元素自动支持点击事件
  });
</script>

(二)React 合成事件示例:计数器组件

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = (e) => {
    console.log('合成事件类型:', e.type); // 输出'click'
    console.log('原生事件对象:', e.nativeEvent); // 访问原生事件
    setCount(count + 1);
  };

  return (
    <div>
      <p>点击次数:{count}</p>
      <button onClick={handleClick}>点击我</button>
    </div>
  );
}

五、总结:事件机制的 “终极奥义”

从 JS 原生的事件冒泡、捕获,到 React 的事件委托和合成事件,核心思想始终是 “高效管理”—— 用最少的监听器处理最多的事件,让代码更简洁、性能更优。下次遇到事件相关的问题,记得从这几个维度思考:

  1. 事件流走到哪一步了?捕获还是冒泡?

  2. 用事件委托能减少监听器数量吗?

  3. React 合成事件需要注意对象回收和跨浏览器兼容吗?

掌握这些,你就能轻松驾驭前端事件的 “七十二变”,写出又快又稳的交互代码啦