Skip to content

合成事件系统

React 事件机制的设计与实现原理

1. 什么是合成事件?

1.1 核心概念

合成事件 (SyntheticEvent) 是 React 对原生 DOM 事件的跨浏览器封装。

jsx
function Button() {
    const handleClick = (event) => {
        // event 是 SyntheticEvent,不是原生 Event
        console.log(event);                    // SyntheticBaseEvent
        console.log(event.nativeEvent);        // 原生 MouseEvent
        console.log(event.target);             // 触发元素
        console.log(event.currentTarget);      // 绑定元素
    };
    
    return <button onClick={handleClick}>Click</button>;
}

1.2 为什么需要合成事件?

┌─────────────────────────────────────────────────────────────────┐
│                    合成事件的优势                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 跨浏览器兼容                                                │
│     统一不同浏览器的事件差异                                     │
│                                                                 │
│  2. 性能优化                                                    │
│     事件委托,减少事件监听器数量                                 │
│                                                                 │
│  3. 统一事件流                                                  │
│     所有事件行为一致                                            │
│                                                                 │
│  4. 更好的控制                                                  │
│     可以实现优先级调度等特性                                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2. 事件委托机制

2.1 React 16 的事件委托

┌─────────────────────────────────────────────────────────────────┐
│                    React 16 事件委托                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  document                                                       │
│     │                                                           │
│     └── 所有事件监听器都绑定在 document 上                       │
│                                                                 │
│  <div id="root">                                                │
│      <App>                                                      │
│          <button onClick={...}>  ← 事件冒泡到 document           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2.2 React 17+ 的变化

┌─────────────────────────────────────────────────────────────────┐
│                    React 17+ 事件委托                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  document                                                       │
│     │                                                           │
│  <div id="root">  ← 事件监听器绑定在 root 容器上                 │
│      <App>                                                      │
│          <button onClick={...}>                                 │
│                                                                 │
│  优势:                                                          │
│  - 多个 React 版本可以共存                                      │
│  - 与其他库的事件系统兼容更好                                    │
│  - 微前端场景更友好                                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2.3 事件触发流程

javascript
// 用户点击 button
// 1. 原生事件冒泡到 root 容器
// 2. React 收集从 target 到 root 路径上所有的 onClick 处理函数
// 3. 按顺序执行(模拟冒泡或捕获)

// 简化的内部实现
function dispatchEvent(nativeEvent) {
    const target = nativeEvent.target;
    const path = [];
    
    // 收集路径上所有 fiber
    let fiber = getFiberFromNode(target);
    while (fiber) {
        path.push(fiber);
        fiber = fiber.return;
    }
    
    // 捕获阶段(从上到下)
    for (let i = path.length - 1; i >= 0; i--) {
        const onClickCapture = path[i].props.onClickCapture;
        if (onClickCapture) onClickCapture(syntheticEvent);
    }
    
    // 冒泡阶段(从下到上)
    for (let i = 0; i < path.length; i++) {
        const onClick = path[i].props.onClick;
        if (onClick) onClick(syntheticEvent);
    }
}

3. 事件池 (已废弃)

3.1 React 16 的事件池

jsx
// React 16 中,SyntheticEvent 会被复用
function handleClick(event) {
    console.log(event.type);  // 'click'
    
    setTimeout(() => {
        console.log(event.type);  // null (事件已被回收!)
    }, 0);
}

// 需要 persist() 保持引用
function handleClick(event) {
    event.persist();  // 从池中移除
    setTimeout(() => {
        console.log(event.type);  // 'click' ✅
    }, 0);
}

3.2 React 17+ 移除事件池

jsx
// React 17+ 不再使用事件池
function handleClick(event) {
    setTimeout(() => {
        console.log(event.type);  // 'click' ✅ 无需 persist
    }, 0);
}

移除原因

  1. 现代浏览器性能足够好,事件池优化收益小
  2. 容易造成困惑和 bug
  3. 与 Concurrent Mode 不兼容

4. 事件命名与处理

4.1 命名规范

jsx
// React 事件使用 camelCase
<button onClick={handleClick}>      // ✅
<button onclick={handleClick}>      // ❌ 原生 HTML 写法

// 捕获阶段加 Capture 后缀
<div onClickCapture={handleCapture}>
    <button onClick={handleBubble}>Click</button>
</div>

4.2 常用事件

jsx
// 鼠标事件
onClick, onDoubleClick, onMouseDown, onMouseUp
onMouseEnter, onMouseLeave  // 不冒泡
onMouseOver, onMouseOut     // 冒泡

// 键盘事件
onKeyDown, onKeyUp, onKeyPress (已废弃)

// 表单事件
onChange, onInput, onSubmit, onFocus, onBlur

// 触摸事件
onTouchStart, onTouchMove, onTouchEnd

// 其他
onScroll, onWheel, onCopy, onPaste

4.3 onChange 的特殊性

jsx
// React 的 onChange 实际上是 onInput
// 每次输入都触发,而不是失焦时

<input onChange={e => console.log(e.target.value)} />
// 输入 'abc':打印 'a', 'ab', 'abc'

// 原生 change 事件需要用 onBlur
<input onBlur={e => console.log(e.target.value)} />

5. 阻止默认行为与冒泡

5.1 preventDefault 和 stopPropagation

jsx
function Form() {
    const handleSubmit = (e) => {
        e.preventDefault();      // 阻止表单提交
        console.log('submit');
    };
    
    const handleClick = (e) => {
        e.stopPropagation();     // 阻止冒泡
        console.log('click');
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <button onClick={handleClick}>Submit</button>
        </form>
    );
}

5.2 与原生事件混用的问题

jsx
function Component() {
    const ref = useRef(null);
    
    useEffect(() => {
        // 原生事件监听
        const handleNativeClick = (e) => {
            console.log('native');
        };
        ref.current.addEventListener('click', handleNativeClick);
        return () => ref.current.removeEventListener('click', handleNativeClick);
    }, []);
    
    const handleReactClick = (e) => {
        e.stopPropagation();  // 只能阻止 React 合成事件的冒泡
        console.log('react');
    };
    
    // 点击会打印: 'native', 'react'
    // 因为原生事件先触发(直接绑定在元素上)
    return <button ref={ref} onClick={handleReactClick}>Click</button>;
}

5.3 阻止原生事件冒泡

jsx
function Component() {
    const handleClick = (e) => {
        e.stopPropagation();           // 阻止 React 事件冒泡
        e.nativeEvent.stopImmediatePropagation();  // 阻止原生事件
    };
    
    return <button onClick={handleClick}>Click</button>;
}

6. 事件处理最佳实践

6.1 传递参数

jsx
// 方式一:箭头函数(每次渲染创建新函数)
<button onClick={() => handleClick(id)}>Delete</button>

// 方式二:bind(每次渲染创建新函数)
<button onClick={handleClick.bind(null, id)}>Delete</button>

// 方式三:data 属性(推荐,不创建新函数)
<button data-id={id} onClick={handleClick}>Delete</button>

function handleClick(e) {
    const id = e.currentTarget.dataset.id;
}

// 方式四:useCallback(需要配合 memo 才有意义)
const handleClick = useCallback(() => {
    deleteItem(id);
}, [id]);

6.2 事件处理函数命名

jsx
// 推荐的命名规范
const handleClick = () => {};       // handle + 事件名
const handleSubmit = () => {};
const handleInputChange = () => {}; // handle + 元素 + 事件名

// 作为 props 传递时
<Child onClick={handleClick} />     // on + 事件名
<Child onItemDelete={handleDelete} /> // on + 描述 + 动作

7. 面试高频问题

Q1: React 事件和原生事件的区别?

区别React 合成事件原生事件
命名camelCase (onClick)全小写 (onclick)
绑定位置root 容器元素本身
事件对象SyntheticEventEvent
阻止默认e.preventDefault()return false 也可以

Q2: React 17 事件系统有什么变化?

  1. 事件委托从 document 改为 root 容器
  2. 移除事件池
  3. 对齐浏览器行为(onScroll 不再冒泡等)

Q3: 为什么 React 使用事件委托?

  1. 减少内存占用:不用给每个元素绑定监听器
  2. 动态元素:新增元素自动具有事件处理能力
  3. 统一管理:便于实现优先级调度等特性

Q4: e.stopPropagation() 能阻止原生事件吗?

不能。React 的 stopPropagation 只阻止 React 合成事件的冒泡。要阻止原生事件,需要使用 e.nativeEvent.stopImmediatePropagation()

Q5: React 的 onChange 和原生的 change 有什么区别?

区别React onChange原生 change
触发时机每次输入都触发失焦时触发
实际监听input 事件change 事件
使用场景实时响应输入完成输入后处理

Q6: 如何在 React 中使用原生事件?

jsx
useEffect(() => {
    const handler = (e) => { ... };
    element.addEventListener('event', handler);
    return () => element.removeEventListener('event', handler);
}, []);

注意:原生事件先于 React 事件触发,混用时需小心顺序问题。

前端面试知识库