五、React 合成事件系统:架构设计的优雅解决方案

22 阅读15分钟

核心问题:如何优雅地处理兼容性与性能?

问题的起源

想象你要在 React 中处理 1000 个按钮的点击事件:

方案 A:原生事件绑定

function ButtonList() {
  return (
    <div>
      {Array(1000).fill(0).map((_, i) => (
        <button onclick={handleClick}>按钮 {i}</button>
      ))}
    </div>
  );
}

面临的问题:

  1. 内存消耗:1000 个按钮 = 1000 个事件监听器 = 大量内存
  2. 浏览器兼容性
    // IE:event.srcElement
    // 现代浏览器:event.target
    // IE:event.returnValue = false
    // 现代浏览器:event.preventDefault()
    
  3. 事件对象差异:每个浏览器的 event 对象结构不同
  4. 内存泄漏风险:组件卸载时需要手动移除监听器

React 的答案:合成事件系统(Synthetic Event System)


架构设计思路:三层解决方案

┌─────────────────────────────────────────────────────────┐
│                   架构分层                               │
├─────────────────────────────────────────────────────────┤
│  第 1 层:事件代理 (Event Delegation)                   │
│    ↓  减少监听器数量:1000 个按钮 → 1 个监听器          │
├─────────────────────────────────────────────────────────┤
│  第 2 层:适配器模式 (Adapter Pattern)                  │
│    ↓  统一事件接口:抹平浏览器差异                      │
├─────────────────────────────────────────────────────────┤
│  第 3 层:对象池复用 (Object Pooling)                   │
│    ↓  减少 GC 压力:复用事件对象                        │
└─────────────────────────────────────────────────────────┘

架构智慧:

  • 用代理模式解决内存问题
  • 用适配器模式解决兼容性问题
  • 用对象池模式解决性能问题

第 1 层:事件代理(Event Delegation)

什么是事件代理?

传统方式:为每个元素绑定监听器

<div>
  <button id="btn1">按钮1</button>  ← 绑定监听器
  <button id="btn2">按钮2</button>  ← 绑定监听器
  <button id="btn3">按钮3</button>  ← 绑定监听器
</div>

<script>
document.getElementById('btn1').addEventListener('click', handler1);
document.getElementById('btn2').addEventListener('click', handler2);
document.getElementById('btn3').addEventListener('click', handler3);
</script>

内存占用:

  • 3 个按钮 = 3 个监听器
  • 1000 个按钮 = 1000 个监听器(每个约 100-200 字节)
  • 总计:约 100KB-200KB 内存

事件代理:利用事件冒泡,在父元素统一监听

<div id="container">
  <button data-id="1">按钮1</button>
  <button data-id="2">按钮2</button>
  <button data-id="3">按钮3</button>
</div>

<script>
// 只在父元素绑定一个监听器
document.getElementById('container').addEventListener('click', function(e) {
  if (e.target.tagName === 'BUTTON') {
    const id = e.target.dataset.id;
    console.log('点击了按钮', id);
  }
});
</script>

内存占用:

  • 3 个按钮 = 1 个监听器
  • 1000 个按钮 = 1 个监听器(约 100-200 字节)
  • 总计:约 100-200 字节(节省 99.9%)

React 的事件代理机制

React 16 及之前:在 document 上代理

┌──────────────────────────────────────┐
│  document ← 所有事件都在这里监听     │
├──────────────────────────────────────┤
│    <div id="root">                   │
│      <button onClick={handler}>      │
│        点击我                         │
│      </button>                       │
│    </div>                            │
└──────────────────────────────────────┘

事件流:button 点击 → 冒泡到 document → React 处理

React 17+:在 root 容器上代理

┌──────────────────────────────────────┐
│  document                            │
│    ↓                                 │
│    <div id="root"> ← 在这里监听      │
│      <button onClick={handler}>      │
│        点击我                         │
│      </button>                       │
│    </div>                            │
└──────────────────────────────────────┘

优势:支持多个 React 应用共存(微前端场景)

事件代理的完整流程

// 1. 开发者写的代码
function App() {
  const handleClick = (e) => {
    console.log('点击了', e.target);
  };

  return <button onClick={handleClick}>点击我</button>;
}

React 内部的处理(简化版):

// 2. React 渲染时的处理
function render() {
  // ❌ React 不会这样做:
  // buttonElement.addEventListener('click', handleClick);

  // ✅ React 实际的做法:
  // 2.1 记录事件和处理函数的映射关系
  eventRegistry.set(buttonElement, {
    click: handleClick
  });

  // 2.2 如果是第一次遇到 click 事件,在 root 上注册代理
  if (!delegatedEvents.has('click')) {
    rootElement.addEventListener('click', dispatchEvent);
    delegatedEvents.add('click');
  }
}

// 3. 用户点击按钮时
function dispatchEvent(nativeEvent) {
  // 3.1 获取真正被点击的元素
  const targetElement = nativeEvent.target;

  // 3.2 从目标元素向上收集所有 React 事件处理器
  const eventPath = [];
  let current = targetElement;

  while (current) {
    const handler = eventRegistry.get(current)?.click;
    if (handler) {
      eventPath.push({ element: current, handler });
    }
    current = current.parentElement;
  }

  // 3.3 创建合成事件对象
  const syntheticEvent = createSyntheticEvent(nativeEvent);

  // 3.4 按顺序执行处理器(模拟捕获和冒泡)
  for (const { handler } of eventPath) {
    handler(syntheticEvent);

    if (syntheticEvent.isPropagationStopped()) {
      break; // 停止冒泡
    }
  }
}

事件代理的架构优势

1. 内存优化

// 传统方式:
1000 个元素 × 100 字节 = 100KB

// React 方式:
1 个代理监听器 × 100 字节 = 100 字节
1000 个映射记录 × 20 字节 = 20KB
总计:约 20KB(节省 80%)

2. 动态绑定

// 元素动态增删,无需管理监听器
function DynamicList() {
  const [items, setItems] = useState([1, 2, 3]);

  return (
    <div>
      {items.map(item => (
        <button key={item} onClick={handleClick}>
          按钮 {item}
        </button>
      ))}
      <button onClick={() => setItems([...items, items.length + 1])}>
        添加按钮
      </button>
    </div>
  );
}

// React 自动处理:
// - 新按钮:自动加入映射
// - 删除按钮:自动清理映射
// - 无需手动 addEventListener/removeEventListener

3. 统一管理

// 所有事件在一个地方处理
// 方便实现:
// - 事件优先级
// - 事件调度
// - 批量更新

第 2 层:适配器模式(Adapter Pattern)

问题:浏览器兼容性噩梦

不同浏览器的差异:

// 1. 事件目标
IE:        event.srcElement
标准浏览器: event.target

// 2. 阻止默认行为
IE:        event.returnValue = false
标准浏览器: event.preventDefault()

// 3. 停止冒泡
IE:        event.cancelBubble = true
标准浏览器: event.stopPropagation()

// 4. 鼠标按键
IE:        event.button (1=左键, 2=右键, 4=中键)
标准浏览器: event.button (0=左键, 1=中键, 2=右键)

// 5. 事件对象获取
IEwindow.event
标准浏览器: 函数参数

// ... 还有数十个差异

适配器模式:统一接口

核心思想:用一个中间层抹平差异

原生事件对象 (各不相同)
      ↓
  [适配器层]
      ↓
合成事件对象 (统一接口)

React 的合成事件对象

// React 的 SyntheticEvent 类(简化版)
class SyntheticEvent {
  constructor(nativeEvent) {
    this.nativeEvent = nativeEvent;

    // 1. 统一属性访问
    this.target = nativeEvent.target || nativeEvent.srcElement;
    this.currentTarget = nativeEvent.currentTarget;
    this.type = nativeEvent.type;

    // 2. 统一鼠标按键
    this.button = this.normalizeButton(nativeEvent.button);

    // 3. 统一键盘事件
    this.key = this.normalizeKey(nativeEvent);

    // 4. 其他属性...
    this.bubbles = nativeEvent.bubbles;
    this.cancelable = nativeEvent.cancelable;
    this.timeStamp = nativeEvent.timeStamp;
  }

  // 统一的方法接口
  preventDefault() {
    const e = this.nativeEvent;
    if (e.preventDefault) {
      e.preventDefault();
    } else {
      e.returnValue = false; // IE
    }
    this.defaultPrevented = true;
  }

  stopPropagation() {
    const e = this.nativeEvent;
    if (e.stopPropagation) {
      e.stopPropagation();
    } else {
      e.cancelBubble = true; // IE
    }
    this.isPropagationStopped = () => true;
  }

  // 适配器方法:标准化按键
  normalizeButton(button) {
    // IE 的按键编码转换
    if (isIE) {
      return button === 1 ? 0 : button === 4 ? 1 : button === 2 ? 2 : button;
    }
    return button;
  }

  // 适配器方法:标准化键盘按键
  normalizeKey(nativeEvent) {
    // 统一 key/keyCode/which
    if (nativeEvent.key) {
      return nativeEvent.key;
    }
    const keyCode = nativeEvent.keyCode || nativeEvent.which;
    return keyCodeToKey[keyCode] || 'Unknown';
  }
}

使用示例:开发者无需关心兼容性

function MyComponent() {
  const handleClick = (e) => {
    // ✅ 统一的接口,在所有浏览器都能工作
    console.log(e.target);        // 统一获取目标元素
    console.log(e.button);        // 统一的按键编码
    e.preventDefault();           // 统一的方法调用
    e.stopPropagation();          // 统一的方法调用

    // ❌ 不需要写这样的兼容代码:
    // const target = e.target || e.srcElement;
    // if (e.preventDefault) {
    //   e.preventDefault();
    // } else {
    //   e.returnValue = false;
    // }
  };

  return <button onClick={handleClick}>点击</button>;
}

适配器模式的架构价值

1. 关注点分离

开发者:只关心业务逻辑
React:处理浏览器兼容性
浏览器:提供原生能力

2. 统一接口

// 同一套代码在所有浏览器运行
const handler = (e) => {
  e.preventDefault();  // 无需判断浏览器类型
};

3. 易于维护

// 浏览器兼容代码集中管理
// 需要修改时,只改一个地方
// 所有组件自动受益

第 3 层:对象池复用(Object Pooling)

问题:频繁创建对象导致 GC 压力

场景分析:

function MouseTracker() {
  const handleMouseMove = (e) => {
    console.log(e.clientX, e.clientY);
  };

  return <div onMouseMove={handleMouseMove}>移动鼠标</div>;
}

性能瓶颈:

鼠标移动:60fps
每秒触发:60 次 mousemove 事件
每次创建:1 个 SyntheticEvent 对象
每秒创建:60 个对象

长时间运行:
1 分钟 = 3600 个对象
10 分钟 = 36000 个对象

结果:
- 内存占用增加
- 垃圾回收频繁
- 可能导致卡顿

对象池模式:复用事件对象

核心思想:事件处理完后,清空并放回池中,下次复用

// 事件对象池(简化版)
class EventPool {
  constructor() {
    this.pool = [];
  }

  // 获取事件对象
  getPooled(nativeEvent) {
    // 如果池中有空闲对象,复用
    if (this.pool.length > 0) {
      const event = this.pool.pop();
      event.init(nativeEvent);  // 重新初始化
      return event;
    }

    // 池中没有,创建新对象
    return new SyntheticEvent(nativeEvent);
  }

  // 释放事件对象
  release(event) {
    // 清空事件对象的所有属性
    event.target = null;
    event.currentTarget = null;
    event.nativeEvent = null;
    event._dispatchInstances = null;
    // ... 清空所有属性

    // 放回池中
    if (this.pool.length < 10) {  // 限制池的大小
      this.pool.push(event);
    }
  }
}

const eventPool = new EventPool();

React 的事件处理流程(含对象池)

// React 内部的事件分发(简化版)
function dispatchEvent(nativeEvent) {
  // 1. 从池中获取合成事件对象
  const syntheticEvent = eventPool.getPooled(nativeEvent);

  try {
    // 2. 执行事件处理器
    eventHandler(syntheticEvent);
  } finally {
    // 3. 处理完成后,释放对象回池
    eventPool.release(syntheticEvent);
  }
}

重要警告:事件对象会被回收

function MyComponent() {
  const handleClick = (e) => {
    console.log(e.type);  // ✅ 同步访问:OK

    setTimeout(() => {
      console.log(e.type);  // ❌ 异步访问:undefined!
    }, 100);

    // 原因:事件处理器执行完后,e 已被回收
  };

  return <button onClick={handleClick}>点击</button>;
}

解决方案:持久化事件对象

function MyComponent() {
  const handleClick = (e) => {
    // 方法 1:调用 persist()
    e.persist();  // 告诉 React:不要回收这个对象
    setTimeout(() => {
      console.log(e.type);  // ✅ 现在可以异步访问
    }, 100);

    // 方法 2:提前保存需要的值
    const eventType = e.type;
    const target = e.target;
    setTimeout(() => {
      console.log(eventType, target);  // ✅ 访问保存的值
    }, 100);
  };

  return <button onClick={handleClick}>点击</button>;
}

注意:React 17+ 已移除对象池

// React 17+:不再使用对象池
// 原因:现代浏览器的 GC 性能已足够好
// 好处:不再需要 e.persist()
// 可以直接在异步代码中使用事件对象

对象池模式的架构价值

1. 减少内存分配

没有对象池:
每次事件 → 分配内存 → 执行 → GC 回收
60 fps = 每秒 60 次分配和回收

有对象池:
第一次事件 → 分配内存 → 执行 → 放回池
后续事件 → 从池取出 → 执行 → 放回池
内存分配次数大大减少

2. 减少 GC 压力

// 对比测试(假设数据)
无对象池:
  - GC 频率:每 2 秒一次
  - 每次 GC10-20ms
  - 可能导致掉帧

有对象池:
  - GC 频率:每 10 秒一次
  - 每次 GC5-10ms
  - 流畅度提升

完整流程:从点击到处理

用户点击按钮的完整旅程

// 1. 开发者代码
function App() {
  const handleClick = (e) => {
    console.log('点击了', e.target.textContent);
    e.preventDefault();
  };

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

内部执行流程:

┌──────────────────────────────────────────────────────────┐
│  第 1 步:用户点击按钮                                   │
│    button 元素被点击                                     │
└──────────────────────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────┐
│  第 2 步:原生事件冒泡                                   │
│    click 事件从 button 冒泡到 root 容器                 │
└──────────────────────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────┐
│  第 3 步:React 的代理监听器捕获                         │
│    root.addEventListener('click', dispatchEvent)         │
│    触发 dispatchEvent(nativeEvent)                       │
└──────────────────────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────┐
│  第 4 步:创建合成事件对象(适配器层)                   │
│    syntheticEvent = eventPool.getPooled(nativeEvent)     │
│    - 统一接口                                            │
│    - 抹平浏览器差异                                      │
└──────────────────────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────┐
│  第 5 步:收集事件路径                                   │
│    从 button 向上遍历到 root,收集所有 onClick 处理器    │
│    eventPath = [                                         ││      { fiber: buttonFiber, handler: handleClick },       ││      // ... 父级处理器                                   ││    ]                                                     │
└──────────────────────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────┐
│  第 6 步:执行处理器(模拟冒泡)                         │
│    for (const { handler } of eventPath) {                │
│      handler(syntheticEvent)                             │
│      if (syntheticEvent.isPropagationStopped()) break    │
│    }                                                     │
└──────────────────────────────────────────────────────────┘
                         ↓
┌──────────────────────────────────────────────────────────┐
│  第 7 步:回收事件对象                                   │
│    eventPool.release(syntheticEvent)                     │
│    清空属性,放回池中供下次使用                          │
└──────────────────────────────────────────────────────────┘

架构设计的核心智慧

1. 代理模式(Proxy Pattern)

定义:为其他对象提供一个代理,以控制对这个对象的访问

// 不用代理:直接访问
element1.addEventListener('click', handler1);
element2.addEventListener('click', handler2);
// ... 管理复杂

// 使用代理:统一入口
root.addEventListener('click', proxyHandler);
// proxyHandler 内部路由到具体处理器

优势:

  • 集中控制:所有事件在一个地方管理
  • 减少资源:少量监听器替代大量监听器
  • 易于扩展:可以在代理层加入优先级、调度等功能

其他应用场景:

  • 图片懒加载:代理 img 标签,统一管理加载
  • API 请求:代理 fetch,统一处理认证、错误
  • 权限控制:代理对象访问,统一验证权限

2. 适配器模式(Adapter Pattern)

定义:将一个类的接口转换成客户期望的另一个接口

// 不同的"电源插座"(浏览器)
IE:      { srcElement, returnValue }
Chrome:  { target, preventDefault() }
Firefox: { target, preventDefault() }

// 适配器:统一的"转换器"
SyntheticEvent: { target, preventDefault() }

// 用户只需使用统一接口
handler(syntheticEvent) {
  syntheticEvent.target         // 统一!
  syntheticEvent.preventDefault() // 统一!
}

优势:

  • 隔离变化:浏览器接口变化不影响业务代码
  • 统一接口:开发者无需学习各浏览器差异
  • 易于测试:mock 统一的合成事件即可

其他应用场景:

  • 第三方库集成:适配不同的 UI 库接口
  • 数据格式转换:后端数据 → 前端数据结构
  • 多端适配:Web/App/小程序统一接口

3. 对象池模式(Object Pool Pattern)

定义:预先创建一定数量的对象,复用以减少创建和销毁开销

// 不用对象池:每次创建新对象
for (let i = 0; i < 1000; i++) {
  const obj = new BigObject();  // 创建
  use(obj);
  // obj 等待 GC 回收
}

// 使用对象池:复用对象
const pool = new ObjectPool(BigObject, 10);
for (let i = 0; i < 1000; i++) {
  const obj = pool.acquire();   // 从池中取
  use(obj);
  pool.release(obj);            // 放回池中
}

优势:

  • 减少分配:降低内存分配频率
  • 减少 GC:减少垃圾回收压力
  • 性能稳定:避免 GC 导致的卡顿

其他应用场景:

  • 数据库连接池:复用数据库连接
  • 线程池:复用线程
  • Canvas 对象池:游戏中复用精灵对象

实战理解:事件系统的细节

1. 事件优先级

React 对事件进行分级,确保用户交互优先

// 事件优先级(简化)
const EventPriority = {
  // 离散事件:用户直接交互(最高优先级)
  DiscreteEvent: 0,  // click, keydown, focus, blur

  // 用户阻塞事件:用户期待立即反馈
  UserBlockingEvent: 1,  // drag, scroll, mousemove

  // 连续事件:可以延迟处理
  ContinuousEvent: 2,  // animation, layout
};

// 根据优先级调度
function dispatchEvent(nativeEvent) {
  const priority = getEventPriority(nativeEvent.type);

  if (priority === EventPriority.DiscreteEvent) {
    // 立即同步执行
    flushSync(() => {
      executeHandler(nativeEvent);
    });
  } else {
    // 可以批量处理或延迟
    scheduleCallback(priority, () => {
      executeHandler(nativeEvent);
    });
  }
}

效果:

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

  const handleClick = () => {
    setCount(count + 1);  // 高优先级,立即更新
  };

  const handleScroll = () => {
    setCount(count + 1);  // 低优先级,可能延迟
  };

  return (
    <div>
      <button onClick={handleClick}>点击</button>  {/* 即时响应 */}
      <div onScroll={handleScroll}>滚动区域</div>  {/* 可能节流 */}
    </div>
  );
}

2. 事件捕获与冒泡

React 模拟了完整的事件传播机制

function App() {
  return (
    <div
      onClick={() => console.log('div 冒泡')}
      onClickCapture={() => console.log('div 捕获')}
    >
      <button
        onClick={() => console.log('button 冒泡')}
        onClickCapture={() => console.log('button 捕获')}
      >
        点击我
      </button>
    </div>
  );
}

// 点击按钮后的输出顺序:
// 1. div 捕获
// 2. button 捕获
// 3. button 冒泡
// 4. div 冒泡

实现原理:

function dispatchEvent(nativeEvent) {
  const targetElement = nativeEvent.target;
  const eventPath = [];

  // 收集路径:从 root 到 target
  let current = targetElement;
  while (current) {
    eventPath.unshift(current);  // 插入到头部
    current = current.parentElement;
  }

  const syntheticEvent = createSyntheticEvent(nativeEvent);

  // 捕获阶段:从 root 到 target
  for (let i = 0; i < eventPath.length; i++) {
    const handler = getHandler(eventPath[i], 'onClickCapture');
    if (handler) handler(syntheticEvent);
  }

  // 冒泡阶段:从 target 到 root
  for (let i = eventPath.length - 1; i >= 0; i--) {
    const handler = getHandler(eventPath[i], 'onClick');
    if (handler) handler(syntheticEvent);
    if (syntheticEvent.isPropagationStopped()) break;
  }
}

3. 原生事件与合成事件的关系

注意:混用可能导致问题

function App() {
  const buttonRef = useRef(null);

  useEffect(() => {
    // 原生事件监听
    buttonRef.current.addEventListener('click', () => {
      console.log('原生事件');
    });
  }, []);

  const handleClick = () => {
    console.log('React 事件');
  };

  return <button ref={buttonRef} onClick={handleClick}>点击</button>;
}

// 点击后的输出顺序:
// 1. 原生事件
// 2. React 事件

// 原因:
// 原生事件直接在 button 上监听
// React 事件在 root 上监听(冒泡后才到达)

最佳实践:避免混用

// ❌ 不推荐:混用
useEffect(() => {
  element.addEventListener('click', handler);
}, []);

// ✅ 推荐:只用 React 事件
<button onClick={handler}>点击</button>

// ✅ 如果必须用原生事件,注意清理
useEffect(() => {
  element.addEventListener('click', handler);
  return () => {
    element.removeEventListener('click', handler);
  };
}, []);

性能对比:合成事件 vs 原生事件

实验:1000 个按钮的性能对比

// 方案 A:原生事件
function NativeEventButtons() {
  const buttons = [];
  for (let i = 0; i < 1000; i++) {
    const button = document.createElement('button');
    button.textContent = `按钮 ${i}`;
    button.addEventListener('click', () => {
      console.log(`点击了 ${i}`);
    });
    buttons.push(button);
  }
  return buttons;
}

// 方案 B:React 合成事件
function SyntheticEventButtons() {
  const buttons = [];
  for (let i = 0; i < 1000; i++) {
    buttons.push(
      <button key={i} onClick={() => console.log(`点击了 ${i}`)}>
        按钮 {i}
      </button>
    );
  }
  return <div>{buttons}</div>;
}

性能指标:

初始渲染时间:
  原生事件:~150ms (需要绑定 1000 个监听器)
  合成事件:~100ms (只绑定 1 个监听器)

内存占用:
  原生事件:~200KB (1000 个监听器)
  合成事件:~50KB  (1 个监听器 + 映射表)

事件触发延迟:
  原生事件:~0.1ms  (直接调用)
  合成事件:~0.2ms  (经过代理层)

动态增删性能:
  原生事件:需要手动管理监听器
  合成事件:自动管理,无需额外代码

结论:

  • 大量元素:合成事件明显优势(内存、初始化时间)
  • 少量元素:性能差异可忽略
  • 触发延迟:合成事件略高,但在可接受范围(0.1ms)

架构设计的启示

1. 用抽象隔离变化

浏览器差异(变化源)
      ↓
[合成事件层] ← 抽象层,隔离变化
      ↓
业务代码(稳定)

学习要点:

将变化的部分封装起来,暴露稳定的接口给上层


2. 用模式解决通用问题

问题 → 模式映射:

内存优化问题  → 代理模式(集中管理)
兼容性问题    → 适配器模式(统一接口)
性能优化问题  → 对象池模式(复用对象)

学习要点:

设计模式不是"炫技",而是针对特定问题的最佳实践总结


3. 性能优化的层次

第 1 层:减少操作次数
  └─ 事件代理:1000 个监听器 → 1 个

第 2 层:减少操作成本
  └─ 对象池:避免频繁创建/销毁

第 3 层:智能调度
  └─ 事件优先级:关键交互优先处理

学习要点:

性能优化要分层次,从影响最大的层面开始


4. 权衡与取舍

合成事件的权衡:

✅ 收益:
  - 内存优化(代理模式)
  - 跨浏览器兼容(适配器模式)
  - 统一管理(便于扩展)

❌ 代价:
  - 多一层抽象(略增复杂度)
  - 轻微性能开销(0.1ms 延迟)
  - 学习成本(理解合成事件)

结论:收益远大于代价(针对 React 的使用场景)

学习要点:

没有完美的方案,关键是评估收益和代价,做出合适的选择


实践建议

1. 正确使用事件

// ✅ 好:使用 React 事件
<button onClick={handler}>点击</button>

// ❌ 避免:混用原生事件
useEffect(() => {
  buttonRef.current.addEventListener('click', handler);
}, []);

// ✅ 好:在 effect 中使用原生事件,记得清理
useEffect(() => {
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, []);

2. 理解事件对象的生命周期

// ❌ 错误:异步访问事件对象(React 16 及之前)
const handleClick = (e) => {
  setTimeout(() => {
    console.log(e.type);  // undefined(对象已回收)
  }, 100);
};

// ✅ 正确:提前保存需要的值
const handleClick = (e) => {
  const type = e.type;
  setTimeout(() => {
    console.log(type);  // 正常工作
  }, 100);
};

// ✅ 或使用 persist()(React 16 及之前)
const handleClick = (e) => {
  e.persist();
  setTimeout(() => {
    console.log(e.type);  // 正常工作
  }, 100);
};

3. 利用事件捕获

// 场景:在表单提交前验证所有输入
function Form() {
  const handleSubmitCapture = (e) => {
    // 在捕获阶段拦截,可以阻止所有子元素的提交
    const inputs = e.currentTarget.querySelectorAll('input');
    const invalid = Array.from(inputs).some(input => !input.value);

    if (invalid) {
      e.preventDefault();
      e.stopPropagation();
      alert('请填写所有字段');
    }
  };

  return (
    <form onSubmitCapture={handleSubmitCapture}>
      <input name="username" />
      <input name="email" />
      <button type="submit">提交</button>
    </form>
  );
}

总结:架构设计的智慧

合成事件系统教会我们:

  1. 用抽象隔离变化

    • 浏览器千变万化,接口始终如一
    • 变化封装在抽象层,业务代码不受影响
  2. 用模式解决通用问题

    • 代理模式:集中管理,减少资源
    • 适配器模式:统一接口,抹平差异
    • 对象池模式:复用对象,优化性能
  3. 分层设计,职责单一

    • 代理层:管理监听器
    • 适配层:统一接口
    • 优化层:对象池、优先级
  4. 权衡收益与代价

    • 增加抽象层带来复杂度
    • 但换来了更好的性能和开发体验
    • 针对实际场景评估,做出选择

最终启示:

优秀的架构不是追求技术的极致,而是在约束条件下(浏览器兼容性、内存限制、性能要求),通过合理的模式和抽象,找到收益和代价的最佳平衡点。

从今天开始,当你遇到类似问题时,问自己:

  1. 能否用代理模式集中管理?
  2. 能否用适配器模式统一接口?
  3. 能否用对象池模式优化性能?
  4. 收益和代价如何权衡?

这就是 React 合成事件系统给我们的架构智慧。