Skip to content

合成事件系统

学习目标

通过本章学习,你将深入理解:

  • React合成事件系统的原理
  • 合成事件与原生事件的区别
  • 事件池机制(React 17前)
  • 事件委托的实现原理
  • 捕获和冒泡阶段
  • 原生事件与React事件的混用
  • React 19的事件系统优化

第一部分:合成事件系统概述

1.1 什么是合成事件

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

合成事件的优势

jsx
// 原生事件的问题
// 1. 浏览器兼容性问题
element.addEventListener('click', (e) => {
  e.stopPropagation();  // 某些旧浏览器可能不支持
});

// 2. 事件对象属性不一致
// IE: window.event
// 现代浏览器: 参数传递

// React合成事件的优势
function SyntheticEventAdvantages() {
  const handleClick = (e) => {
    // 1. 跨浏览器一致的API
    e.stopPropagation();  // 所有浏览器都支持
    
    // 2. 统一的事件对象属性
    console.log(e.target);
    console.log(e.currentTarget);
    
    // 3. 性能优化(事件委托)
    // 4. 与React生态集成
  };
  
  return <button onClick={handleClick}>点击</button>;
}

1.2 合成事件的结构

jsx
function SyntheticEventStructure() {
  const handleClick = (e) => {
    console.log('合成事件对象:', e);
    
    // 合成事件的属性
    console.log('bubbles:', e.bubbles);
    console.log('cancelable:', e.cancelable);
    console.log('currentTarget:', e.currentTarget);
    console.log('defaultPrevented:', e.defaultPrevented);
    console.log('eventPhase:', e.eventPhase);
    console.log('isTrusted:', e.isTrusted);
    console.log('nativeEvent:', e.nativeEvent);  // 原生事件对象
    console.log('target:', e.target);
    console.log('timeStamp:', e.timeStamp);
    console.log('type:', e.type);
    
    // 方法
    e.preventDefault();
    e.stopPropagation();
    e.persist();  // React 17+已废弃
    
    // 访问原生事件
    console.log('原生事件:', e.nativeEvent);
  };
  
  return <button onClick={handleClick}>查看合成事件</button>;
}

1.3 合成事件vs原生事件

jsx
function SyntheticVsNative() {
  const buttonRef = useRef(null);
  
  useEffect(() => {
    const button = buttonRef.current;
    
    // 原生事件监听
    const nativeHandler = (e) => {
      console.log('原生事件:', e);
      console.log('是原生Event对象:', e instanceof Event);
    };
    
    button.addEventListener('click', nativeHandler);
    
    return () => {
      button.removeEventListener('click', nativeHandler);
    };
  }, []);
  
  // React合成事件
  const handleClick = (e) => {
    console.log('合成事件:', e);
    console.log('是合成Event对象:', e instanceof Event);  // false
    console.log('有nativeEvent:', e.nativeEvent instanceof Event);  // true
  };
  
  return <button ref={buttonRef} onClick={handleClick}>点击测试</button>;
}

// 对比总结
/*
原生事件:
- 浏览器原生Event对象
- 可能有兼容性问题
- 直接操作DOM
- 每个元素都有监听器

合成事件:
- React的SyntheticEvent对象
- 跨浏览器一致
- 通过React事件系统
- 自动事件委托
*/

第二部分:事件委托机制

2.1 React的事件委托

jsx
// React 17之前:事件委托到document
// React 17+:事件委托到根容器

function EventDelegation() {
  // 即使有1000个按钮
  const buttons = Array(1000).fill(0).map((_, i) => i);
  
  return (
    <div>
      {buttons.map(i => (
        <button key={i} onClick={() => console.log(i)}>
          按钮 {i}
        </button>
      ))}
    </div>
  );
  
  // React不会为每个按钮添加监听器
  // 而是在根元素添加一个监听器
  // 通过事件冒泡处理所有点击
}

// 原理演示
function DelegationPrinciple() {
  // React内部类似这样实现:
  useEffect(() => {
    const root = document.getElementById('root');
    
    const handleClick = (nativeEvent) => {
      // 找到触发事件的React组件
      const targetFiber = findFiberFromDOM(nativeEvent.target);
      
      // 创建合成事件
      const syntheticEvent = createSyntheticEvent(nativeEvent);
      
      // 调用React组件的onClick处理器
      if (targetFiber.props.onClick) {
        targetFiber.props.onClick(syntheticEvent);
      }
    };
    
    root.addEventListener('click', handleClick);
    
    return () => {
      root.removeEventListener('click', handleClick);
    };
  }, []);
  
  return <div>{/* ... */}</div>;
}

2.2 事件委托的好处

jsx
// 1. 性能优势
function PerformanceAdvantage() {
  const [items] = useState(Array(10000).fill(0).map((_, i) => ({
    id: i,
    name: `Item ${i}`
  })));
  
  const handleClick = (id) => {
    console.log('点击:', id);
  };
  
  // React只在root添加一个监听器
  // 而不是10000个
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => handleClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

// 2. 动态元素支持
function DynamicElements() {
  const [items, setItems] = useState([]);
  
  const addItem = () => {
    setItems([...items, { id: Date.now(), name: 'New' }]);
  };
  
  const handleClick = (id) => {
    console.log(id);
  };
  
  return (
    <div>
      <button onClick={addItem}>添加</button>
      <ul>
        {items.map(item => (
          <li key={item.id} onClick={() => handleClick(item.id)}>
            {item.name}
          </li>
        ))}
      </ul>
      {/* 新添加的元素自动有事件处理 */}
    </div>
  );
}

第三部分:事件冒泡和捕获

3.1 事件传播阶段

jsx
function EventPhases() {
  const handleCapture = (phase) => (e) => {
    console.log(`${phase} 捕获阶段`);
  };
  
  const handleBubble = (phase) => (e) => {
    console.log(`${phase} 冒泡阶段`);
  };
  
  return (
    <div
      onClickCapture={handleCapture('外层')}
      onClick={handleBubble('外层')}
    >
      <div
        onClickCapture={handleCapture('中层')}
        onClick={handleBubble('中层')}
      >
        <button
          onClickCapture={handleCapture('内层')}
          onClick={handleBubble('内层')}
        >
          点击测试
        </button>
      </div>
    </div>
  );
  
  // 点击按钮,输出顺序:
  // 外层 捕获阶段
  // 中层 捕获阶段
  // 内层 捕获阶段
  // 内层 冒泡阶段
  // 中层 冒泡阶段
  // 外层 冒泡阶段
}

3.2 阻止传播

jsx
function StopPropagationDemo() {
  const handleOuter = () => console.log('外层');
  const handleInner = (e) => {
    e.stopPropagation();
    console.log('内层');
  };
  
  return (
    <div onClick={handleOuter}>
      <button onClick={handleInner}>
        点击(不会触发外层)
      </button>
    </div>
  );
}

// stopPropagation详解
function StopPropagationDetails() {
  const handleDocument = () => console.log('Document');
  const handleOuter = () => console.log('外层Div');
  const handleMiddle = () => console.log('中层Div');
  const handleButton = (e) => {
    e.stopPropagation();
    console.log('按钮');
    // 阻止事件继续向上冒泡
  };
  
  useEffect(() => {
    document.addEventListener('click', handleDocument);
    return () => document.removeEventListener('click', handleDocument);
  }, []);
  
  return (
    <div onClick={handleOuter}>
      外层
      <div onClick={handleMiddle}>
        中层
        <button onClick={handleButton}>
          点击(只输出"按钮")
        </button>
      </div>
    </div>
  );
}

3.3 阻止默认行为

jsx
function PreventDefaultDemo() {
  // 阻止链接跳转
  const handleLinkClick = (e) => {
    e.preventDefault();
    console.log('链接被点击,但不会跳转');
    // 自定义导航逻辑
    navigateTo(e.target.href);
  };
  
  // 阻止表单提交
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('表单提交被阻止');
    // 自定义提交逻辑
    submitForm();
  };
  
  // 阻止右键菜单
  const handleContextMenu = (e) => {
    e.preventDefault();
    console.log('右键菜单被禁用');
  };
  
  return (
    <div>
      <a href="https://example.com" onClick={handleLinkClick}>
        点击链接(不会跳转)
      </a>
      
      <form onSubmit={handleSubmit}>
        <input type="text" />
        <button type="submit">提交</button>
      </form>
      
      <div onContextMenu={handleContextMenu}>
        右键点击这里(不会显示菜单)
      </div>
    </div>
  );
}

3.4 事件传播的实际应用

jsx
function EventPropagationApp() {
  const [logs, setLogs] = useState([]);
  
  const addLog = (message) => {
    setLogs(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`]);
  };
  
  const handleOuterCapture = () => addLog('外层-捕获');
  const handleOuter = () => addLog('外层-冒泡');
  const handleMiddleCapture = () => addLog('中层-捕获');
  const handleMiddle = () => addLog('中层-冒泡');
  const handleButtonCapture = () => addLog('按钮-捕获');
  const handleButton = () => addLog('按钮-冒泡');
  
  return (
    <div>
      <div
        style={{ padding: '30px', background: '#f0f0f0' }}
        onClickCapture={handleOuterCapture}
        onClick={handleOuter}
      >
        外层
        <div
          style={{ padding: '20px', background: '#ddd', margin: '10px' }}
          onClickCapture={handleMiddleCapture}
          onClick={handleMiddle}
        >
          中层
          <button
            style={{ margin: '10px' }}
            onClickCapture={handleButtonCapture}
            onClick={handleButton}
          >
            点击我看事件传播
          </button>
        </div>
      </div>
      
      <div style={{ marginTop: '20px' }}>
        <h3>事件日志:</h3>
        <button onClick={() => setLogs([])}>清空日志</button>
        <ul>
          {logs.map((log, i) => (
            <li key={i}>{log}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

第四部分:事件池机制(React 17之前)

4.1 事件池的概念

jsx
// React 16及之前:事件池机制
function EventPooling() {
  const handleClick = (e) => {
    console.log(e.type);  // "click"
    
    // 异步访问事件对象
    setTimeout(() => {
      console.log(e.type);  // React 16: null (事件对象被重用)
                           // React 17+: "click" (不再使用事件池)
    }, 0);
  };
  
  return <button onClick={handleClick}>点击</button>;
}

// React 16中需要persist()
function EventPersist() {
  const handleClick = (e) => {
    e.persist();  // 保持事件对象
    
    setTimeout(() => {
      console.log(e.type);  // "click" (现在可以访问)
    }, 0);
  };
  
  return <button onClick={handleClick}>点击</button>;
}

// React 17+:不再需要persist()
function React17Plus() {
  const handleClick = (e) => {
    // 不需要persist()
    setTimeout(() => {
      console.log(e.type);  // "click" (自动保持)
    }, 0);
  };
  
  return <button onClick={handleClick}>点击</button>;
}

4.2 为什么移除事件池

jsx
// React 16的事件池问题
function EventPoolingProblem() {
  const [events, setEvents] = useState([]);
  
  const handleClick = (e) => {
    // 错误:直接保存事件对象
    setEvents([...events, e]);  // 所有事件都指向同一个对象
    
    // 正确:保存事件属性
    setEvents([...events, {
      type: e.type,
      target: e.target,
      timeStamp: e.timeStamp
    }]);
  };
  
  return (
    <div>
      <button onClick={handleClick}>点击</button>
      <ul>
        {events.map((event, i) => (
          <li key={i}>{event.type} at {event.timeStamp}</li>
        ))}
      </ul>
    </div>
  );
}

// React 17+:可以直接保存事件对象
function React17EventSaving() {
  const [events, setEvents] = useState([]);
  
  const handleClick = (e) => {
    // 现在可以直接保存
    setEvents([...events, e]);
  };
  
  return (
    <div>
      <button onClick={handleClick}>点击</button>
      <ul>
        {events.map((event, i) => (
          <li key={i}>{event.type} at {event.timeStamp}</li>
        ))}
      </ul>
    </div>
  );
}

第五部分:React 17的事件系统变化

5.1 事件委托位置的变化

jsx
// React 16及之前:事件附加到document
// <html>
//   <body>
//     <div id="root">
//       <App />
//     </div>
//   </body>
// </html>
// 所有React事件监听器都附加到document

// React 17+:事件附加到根容器
// <html>
//   <body>
//     <div id="root">  ← 事件监听器附加在这里
//       <App />
//     </div>
//   </body>
// </html>

// 这个变化的好处
function React17Benefits() {
  // 1. 更容易嵌入React到其他框架
  // 2. 避免与其他DOM事件冲突
  // 3. 支持多个React根
  
  return (
    <div>
      {/* 两个独立的React应用 */}
      <div id="app1">
        <ReactApp1 />
      </div>
      <div id="app2">
        <ReactApp2 />
      </div>
    </div>
  );
}

5.2 与原生事件的交互

jsx
function NativeEventInteraction() {
  const buttonRef = useRef(null);
  const [logs, setLogs] = useState([]);
  
  const addLog = (msg) => {
    setLogs(prev => [...prev, msg]);
  };
  
  useEffect(() => {
    const button = buttonRef.current;
    
    // 原生事件(冒泡阶段)
    const nativeHandler = () => {
      addLog('原生事件-冒泡');
    };
    
    // 原生事件(捕获阶段)
    const nativeCaptureHandler = () => {
      addLog('原生事件-捕获');
    };
    
    button.addEventListener('click', nativeHandler);
    button.addEventListener('click', nativeCaptureHandler, true);
    
    // document级别的原生事件
    const documentHandler = () => {
      addLog('Document原生事件');
    };
    document.addEventListener('click', documentHandler);
    
    return () => {
      button.removeEventListener('click', nativeHandler);
      button.removeEventListener('click', nativeCaptureHandler, true);
      document.removeEventListener('click', documentHandler);
    };
  }, []);
  
  // React合成事件
  const handleReactClick = () => {
    addLog('React合成事件-冒泡');
  };
  
  const handleReactClickCapture = () => {
    addLog('React合成事件-捕获');
  };
  
  return (
    <div>
      <button
        ref={buttonRef}
        onClick={handleReactClick}
        onClickCapture={handleReactClickCapture}
      >
        点击测试事件顺序
      </button>
      
      <div>
        <h3>事件触发顺序:</h3>
        <button onClick={() => setLogs([])}>清空</button>
        <ol>
          {logs.map((log, i) => (
            <li key={i}>{log}</li>
          ))}
        </ol>
      </div>
      
      {/* React 17+的执行顺序:
        1. 原生事件-捕获(从document到目标)
        2. React合成事件-捕获
        3. 原生事件-冒泡(在目标元素)
        4. React合成事件-冒泡
        5. Document原生事件
      */}
    </div>
  );
}

5.3 阻止传播的影响

jsx
function StopPropagationImpact() {
  const buttonRef = useRef(null);
  
  useEffect(() => {
    // 原生事件监听器
    const handleNative = () => {
      console.log('原生事件被触发');
    };
    
    document.addEventListener('click', handleNative);
    
    return () => {
      document.removeEventListener('click', handleNative);
    };
  }, []);
  
  // React事件中stopPropagation
  const handleReactClick = (e) => {
    e.stopPropagation();
    console.log('React事件');
  };
  
  return (
    <button ref={buttonRef} onClick={handleReactClick}>
      点击
    </button>
  );
  
  // React 16:
  // - React事件的stopPropagation会阻止document的原生事件
  
  // React 17+:
  // - React事件的stopPropagation只阻止React事件传播
  // - 不会影响原生事件(因为事件附加在root,不是document)
}

第六部分:常见事件类型详解

6.1 鼠标事件

jsx
function MouseEvents() {
  const [info, setInfo] = useState('');
  
  const handleMouseEvent = (type) => (e) => {
    setInfo(`${type}: 
      位置(${e.clientX}, ${e.clientY})
      按钮: ${e.button}
      Alt: ${e.altKey}
      Ctrl: ${e.ctrlKey}
      Shift: ${e.shiftKey}
      Meta: ${e.metaKey}`);
  };
  
  return (
    <div style={{ padding: '20px', background: '#f0f0f0' }}>
      <div
        style={{ padding: '50px', background: 'white', cursor: 'pointer' }}
        onClick={handleMouseEvent('onClick')}
        onDoubleClick={handleMouseEvent('onDoubleClick')}
        onMouseDown={handleMouseEvent('onMouseDown')}
        onMouseUp={handleMouseEvent('onMouseUp')}
        onMouseEnter={handleMouseEvent('onMouseEnter')}
        onMouseLeave={handleMouseEvent('onMouseLeave')}
        onMouseMove={handleMouseEvent('onMouseMove')}
        onMouseOver={handleMouseEvent('onMouseOver')}
        onMouseOut={handleMouseEvent('onMouseOut')}
        onContextMenu={handleMouseEvent('onContextMenu')}
      >
        在这里测试各种鼠标事件
      </div>
      
      <pre>{info}</pre>
    </div>
  );
}

// 鼠标事件的特定应用
function MouseEventApplications() {
  // 1. 拖拽功能
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [dragging, setDragging] = useState(false);
  const [offset, setOffset] = useState({ x: 0, y: 0 });
  
  const handleMouseDown = (e) => {
    setDragging(true);
    setOffset({
      x: e.clientX - position.x,
      y: e.clientY - position.y
    });
  };
  
  const handleMouseMove = (e) => {
    if (dragging) {
      setPosition({
        x: e.clientX - offset.x,
        y: e.clientY - offset.y
      });
    }
  };
  
  const handleMouseUp = () => {
    setDragging(false);
  };
  
  return (
    <div
      onMouseMove={handleMouseMove}
      onMouseUp={handleMouseUp}
      style={{ height: '400px', position: 'relative', background: '#f0f0f0' }}
    >
      <div
        onMouseDown={handleMouseDown}
        style={{
          position: 'absolute',
          left: `${position.x}px`,
          top: `${position.y}px`,
          width: '100px',
          height: '100px',
          background: 'blue',
          cursor: dragging ? 'grabbing' : 'grab',
          userSelect: 'none'
        }}
      >
        拖动我
      </div>
    </div>
  );
}

6.2 键盘事件

jsx
function KeyboardEvents() {
  const [keys, setKeys] = useState([]);
  
  const handleKeyDown = (e) => {
    const keyInfo = {
      key: e.key,
      code: e.code,
      keyCode: e.keyCode,  // 已废弃但仍然可用
      altKey: e.altKey,
      ctrlKey: e.ctrlKey,
      shiftKey: e.shiftKey,
      metaKey: e.metaKey
    };
    
    setKeys(prev => [...prev, keyInfo]);
  };
  
  // 快捷键处理
  const handleShortcut = (e) => {
    // Ctrl+S: 保存
    if (e.ctrlKey && e.key === 's') {
      e.preventDefault();
      console.log('保存快捷键');
    }
    
    // Ctrl+Z: 撤销
    if (e.ctrlKey && e.key === 'z') {
      e.preventDefault();
      console.log('撤销快捷键');
    }
    
    // Esc: 取消
    if (e.key === 'Escape') {
      console.log('ESC键');
    }
    
    // Enter: 确认
    if (e.key === 'Enter') {
      console.log('Enter键');
    }
  };
  
  return (
    <div>
      <input
        type="text"
        onKeyDown={handleKeyDown}
        onKeyUp={handleShortcut}
        placeholder="输入按键测试"
      />
      
      <div>
        <h3>按键记录:</h3>
        <ul>
          {keys.map((key, i) => (
            <li key={i}>
              {key.key} (code: {key.code})
              {key.ctrlKey && ' + Ctrl'}
              {key.altKey && ' + Alt'}
              {key.shiftKey && ' + Shift'}
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

// 键盘导航
function KeyboardNavigation() {
  const [selectedIndex, setSelectedIndex] = useState(0);
  const items = ['项目1', '项目2', '项目3', '项目4', '项目5'];
  
  const handleKeyDown = (e) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setSelectedIndex(i => Math.min(i + 1, items.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setSelectedIndex(i => Math.max(i - 1, 0));
        break;
      case 'Home':
        e.preventDefault();
        setSelectedIndex(0);
        break;
      case 'End':
        e.preventDefault();
        setSelectedIndex(items.length - 1);
        break;
      case 'Enter':
        console.log('选择:', items[selectedIndex]);
        break;
    }
  };
  
  return (
    <div tabIndex={0} onKeyDown={handleKeyDown} style={{ outline: 'none' }}>
      <p>使用方向键导航,Enter选择</p>
      <ul>
        {items.map((item, i) => (
          <li
            key={i}
            style={{
              background: i === selectedIndex ? 'lightblue' : 'white',
              padding: '10px',
              cursor: 'pointer'
            }}
            onClick={() => setSelectedIndex(i)}
          >
            {item}
          </li>
        ))}
      </ul>
    </div>
  );
}

6.3 表单事件

jsx
function FormEvents() {
  const [formData, setFormData] = useState({
    text: '',
    textarea: '',
    select: '',
    checkbox: false,
    radio: ''
  });
  
  // onChange: 值变化时
  const handleChange = (field) => (e) => {
    const value = e.target.type === 'checkbox' 
      ? e.target.checked 
      : e.target.value;
    
    setFormData({ ...formData, [field]: value });
  };
  
  // onInput: 输入时(更频繁)
  const handleInput = (e) => {
    console.log('输入中:', e.target.value);
  };
  
  // onFocus: 获得焦点
  const handleFocus = (e) => {
    console.log('焦点:', e.target.name);
  };
  
  // onBlur: 失去焦点
  const handleBlur = (e) => {
    console.log('失焦:', e.target.name);
  };
  
  // onSubmit: 表单提交
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('提交:', formData);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        name="text"
        value={formData.text}
        onChange={handleChange('text')}
        onInput={handleInput}
        onFocus={handleFocus}
        onBlur={handleBlur}
        placeholder="文本输入"
      />
      
      <textarea
        name="textarea"
        value={formData.textarea}
        onChange={handleChange('textarea')}
        placeholder="多行文本"
      />
      
      <select
        name="select"
        value={formData.select}
        onChange={handleChange('select')}
      >
        <option value="">选择...</option>
        <option value="a">选项A</option>
        <option value="b">选项B</option>
      </select>
      
      <label>
        <input
          type="checkbox"
          checked={formData.checkbox}
          onChange={handleChange('checkbox')}
        />
        复选框
      </label>
      
      <button type="submit">提交</button>
    </form>
  );
}

6.4 触摸事件

jsx
function TouchEvents() {
  const [touches, setTouches] = useState([]);
  
  const handleTouchStart = (e) => {
    const touchList = Array.from(e.touches).map(touch => ({
      id: touch.identifier,
      x: touch.clientX,
      y: touch.clientY
    }));
    setTouches(touchList);
  };
  
  const handleTouchMove = (e) => {
    e.preventDefault();  // 防止页面滚动
    const touchList = Array.from(e.touches).map(touch => ({
      id: touch.identifier,
      x: touch.clientX,
      y: touch.clientY
    }));
    setTouches(touchList);
  };
  
  const handleTouchEnd = () => {
    setTouches([]);
  };
  
  return (
    <div
      onTouchStart={handleTouchStart}
      onTouchMove={handleTouchMove}
      onTouchEnd={handleTouchEnd}
      style={{
        width: '100%',
        height: '300px',
        background: '#f0f0f0',
        position: 'relative',
        touchAction: 'none'  // 禁用默认触摸行为
      }}
    >
      <p>触摸这里</p>
      {touches.map(touch => (
        <div
          key={touch.id}
          style={{
            position: 'absolute',
            left: touch.x,
            top: touch.y,
            width: '50px',
            height: '50px',
            borderRadius: '50%',
            background: 'blue',
            transform: 'translate(-50%, -50%)',
            pointerEvents: 'none'
          }}
        />
      ))}
    </div>
  );
}

// 滑动手势
function SwipeGesture() {
  const [startX, setStartX] = useState(0);
  const [direction, setDirection] = useState('');
  
  const handleTouchStart = (e) => {
    setStartX(e.touches[0].clientX);
  };
  
  const handleTouchEnd = (e) => {
    const endX = e.changedTouches[0].clientX;
    const diff = endX - startX;
    
    if (Math.abs(diff) > 50) {  // 最小滑动距离
      if (diff > 0) {
        setDirection('向右滑动');
      } else {
        setDirection('向左滑动');
      }
    }
  };
  
  return (
    <div
      onTouchStart={handleTouchStart}
      onTouchEnd={handleTouchEnd}
      style={{
        padding: '50px',
        background: '#e0e0e0',
        textAlign: 'center'
      }}
    >
      <p>在这里滑动</p>
      <p>{direction}</p>
    </div>
  );
}

第七部分:性能优化

7.1 事件处理器优化

jsx
// 不好:每次渲染创建新函数
function BadEventHandler() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      {Array(1000).fill(0).map((_, i) => (
        <button key={i} onClick={() => console.log(i)}>
          按钮 {i}
        </button>
      ))}
    </div>
  );
}

// 好:使用useCallback
function GoodEventHandler() {
  const [count, setCount] = useState(0);
  
  const handleClick = useCallback((i) => {
    console.log(i);
  }, []);
  
  return (
    <div>
      {Array(1000).fill(0).map((_, i) => (
        <Button key={i} index={i} onClick={handleClick} />
      ))}
    </div>
  );
}

const Button = React.memo(({ index, onClick }) => {
  return (
    <button onClick={() => onClick(index)}>
      按钮 {index}
    </button>
  );
});

7.2 事件委托优化

jsx
// 使用事件委托处理大列表
function EventDelegationOptimization() {
  const [items] = useState(
    Array(10000).fill(0).map((_, i) => ({ id: i, name: `Item ${i}` }))
  );
  
  // 在父元素统一处理点击
  const handleListClick = (e) => {
    const target = e.target.closest('[data-id]');
    if (target) {
      const id = parseInt(target.dataset.id);
      console.log('点击项:', id);
    }
  };
  
  return (
    <ul onClick={handleListClick}>
      {items.map(item => (
        <li key={item.id} data-id={item.id}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

7.3 防抖和节流

jsx
// 防抖:延迟执行
function DebounceExample() {
  const [value, setValue] = useState('');
  const [debouncedValue, setDebouncedValue] = useState('');
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, 500);
    
    return () => clearTimeout(timer);
  }, [value]);
  
  return (
    <div>
      <input
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="输入搜索..."
      />
      <p>实时值: {value}</p>
      <p>防抖值: {debouncedValue}</p>
    </div>
  );
}

// 节流:限制执行频率
function ThrottleExample() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const lastUpdate = useRef(0);
  
  const handleMouseMove = (e) => {
    const now = Date.now();
    
    // 每100ms最多更新一次
    if (now - lastUpdate.current >= 100) {
      setPosition({ x: e.clientX, y: e.clientY });
      lastUpdate.current = now;
    }
  };
  
  return (
    <div
      onMouseMove={handleMouseMove}
      style={{ height: '200px', background: '#f0f0f0' }}
    >
      <p>鼠标位置: ({position.x}, {position.y})</p>
    </div>
  );
}

第八部分:React 19的事件系统增强

8.1 自动批处理与事件

jsx
// React 19中,所有事件处理器中的状态更新都会自动批处理
function React19AutoBatching() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  const [text, setText] = useState('');
  
  console.log('渲染');  // 只渲染一次
  
  const handleClick = () => {
    setCount(c => c + 1);
    setFlag(f => !f);
    setText('updated');
    // 三个状态更新批处理,只触发一次渲染
  };
  
  // 甚至在异步回调中也会批处理
  const handleAsyncClick = async () => {
    await fetchData();
    setCount(c => c + 1);
    setFlag(f => !f);
    setText('async');
    // React 19: 仍然批处理
    // React 18之前: 会触发三次渲染
  };
  
  return (
    <div>
      <button onClick={handleClick}>同步更新</button>
      <button onClick={handleAsyncClick}>异步更新</button>
      <p>计数: {count}, 标志: {String(flag)}, 文本: {text}</p>
    </div>
  );
}

8.2 新的事件属性

jsx
function React19NewEventProps() {
  const handlePointerEvent = (e) => {
    // React 19增强了指针事件支持
    console.log({
      pointerType: e.pointerType,  // 'mouse', 'pen', 'touch'
      pressure: e.pressure,         // 压力值
      tangentialPressure: e.tangentialPressure,
      tiltX: e.tiltX,
      tiltY: e.tiltY,
      twist: e.twist,
      width: e.width,
      height: e.height
    });
  };
  
  return (
    <div
      onPointerDown={handlePointerEvent}
      onPointerMove={handlePointerEvent}
      onPointerUp={handlePointerEvent}
      style={{ width: '300px', height: '300px', background: '#f0f0f0' }}
    >
      测试指针事件
    </div>
  );
}

第九部分:最佳实践

9.1 参数传递选择

jsx
// 小列表:箭头函数
<button onClick={() => handleClick(id)}>点击</button>

// 大列表:事件委托 + data属性
<ul onClick={handleListClick}>
  {items.map(item => (
    <li data-id={item.id}>{item.name}</li>
  ))}
</ul>

// 性能敏感:useCallback + 提取组件
const MemoItem = React.memo(({ item, onClick }) => (
  <li onClick={onClick}>{item.name}</li>
));

const handleClick = useCallback((id) => {}, []);

9.2 避免内联对象和数组

jsx
// 不好:每次渲染创建新对象
function BadInlineObject() {
  return (
    <Child
      config={{ theme: 'dark', lang: 'zh' }}  // 每次都是新对象
      items={[1, 2, 3]}  // 每次都是新数组
    />
  );
}

// 好:使用useMemo或提取到组件外
const CONFIG = { theme: 'dark', lang: 'zh' };
const ITEMS = [1, 2, 3];

function GoodStableRefs() {
  return (
    <Child
      config={CONFIG}
      items={ITEMS}
    />
  );
}

// 或使用useMemo(如果需要动态值)
function WithMemo({ theme }) {
  const config = useMemo(() => ({
    theme,
    lang: 'zh'
  }), [theme]);
  
  return <Child config={config} />;
}

9.3 事件命名规范

jsx
function EventNaming() {
  // 推荐的命名规范:
  // handleXxx: 事件处理函数
  // onXxx: 回调Props
  
  const handleClick = () => {
    console.log('点击');
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('提交');
  };
  
  const handleChange = (e) => {
    console.log('变化:', e.target.value);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <button onClick={handleClick}>提交</button>
    </form>
  );
}

// 组件Props命名
function MyButton({ onClick, onHover, onFocus }) {
  return (
    <button
      onClick={onClick}
      onMouseEnter={onHover}
      onFocus={onFocus}
    >
      按钮
    </button>
  );
}

9.4 错误处理

jsx
function ErrorHandling() {
  const [error, setError] = useState(null);
  
  const handleClick = async () => {
    try {
      await fetchData();
      setError(null);
    } catch (err) {
      setError(err.message);
      console.error('错误:', err);
    }
  };
  
  const handleInputChange = (e) => {
    try {
      const value = e.target.value;
      if (!value.match(/^[0-9]+$/)) {
        throw new Error('只能输入数字');
      }
      // 处理有效输入
    } catch (err) {
      setError(err.message);
    }
  };
  
  return (
    <div>
      {error && <div className="error">{error}</div>}
      <input onChange={handleInputChange} />
      <button onClick={handleClick}>提交</button>
    </div>
  );
}

第十部分:调试技巧

10.1 事件日志记录

jsx
function EventLogging() {
  const logEvent = (eventName) => (e) => {
    console.group(`🎯 ${eventName}`);
    console.log('事件类型:', e.type);
    console.log('目标元素:', e.target);
    console.log('当前目标:', e.currentTarget);
    console.log('事件阶段:', e.eventPhase);
    console.log('时间戳:', e.timeStamp);
    console.log('是否冒泡:', e.bubbles);
    console.log('是否可取消:', e.cancelable);
    console.log('原生事件:', e.nativeEvent);
    console.groupEnd();
  };
  
  return (
    <div onClick={logEvent('Outer Click')}>
      <button onClick={logEvent('Button Click')}>
        点击查看事件详情
      </button>
    </div>
  );
}

10.2 事件追踪工具

jsx
function EventTracker() {
  const [eventLog, setEventLog] = useState([]);
  
  const trackEvent = (name) => (e) => {
    const eventInfo = {
      name,
      type: e.type,
      target: e.target.tagName,
      timeStamp: e.timeStamp,
      phase: e.eventPhase
    };
    
    setEventLog(prev => [...prev, eventInfo]);
  };
  
  return (
    <div>
      <div
        onClick={trackEvent('外层')}
        onClickCapture={trackEvent('外层捕获')}
        style={{ padding: '20px', background: '#f0f0f0' }}
      >
        外层
        <button
          onClick={trackEvent('按钮')}
          onClickCapture={trackEvent('按钮捕获')}
        >
          点击
        </button>
      </div>
      
      <div>
        <h3>事件日志:</h3>
        <button onClick={() => setEventLog([])}>清空</button>
        <table>
          <thead>
            <tr>
              <th>名称</th>
              <th>类型</th>
              <th>目标</th>
              <th>阶段</th>
              <th>时间</th>
            </tr>
          </thead>
          <tbody>
            {eventLog.map((event, i) => (
              <tr key={i}>
                <td>{event.name}</td>
                <td>{event.type}</td>
                <td>{event.target}</td>
                <td>{event.phase === 1 ? '捕获' : '冒泡'}</td>
                <td>{Math.round(event.timeStamp)}ms</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

10.3 性能分析

jsx
import { Profiler } from 'react';

function PerformanceAnalysis() {
  const [items] = useState(Array(1000).fill(0).map((_, i) => ({
    id: i,
    name: `Item ${i}`
  })));
  
  const handleClick = (id) => {
    console.log('点击:', id);
  };
  
  const onRenderCallback = (
    id,
    phase,
    actualDuration,
    baseDuration
  ) => {
    console.log(`${id} ${phase} 阶段耗时: ${actualDuration}ms`);
  };
  
  return (
    <Profiler id="EventList" onRender={onRenderCallback}>
      <ul>
        {items.map(item => (
          <li key={item.id} onClick={() => handleClick(item.id)}>
            {item.name}
          </li>
        ))}
      </ul>
    </Profiler>
  );
}

第十一部分:常见问题与解决

11.1 问题1:事件处理器不触发

jsx
// 问题:忘记绑定事件
function Problem1() {
  const handleClick = () => {
    console.log('点击');
  };
  
  return <button>点击</button>;  // 忘记 onClick={handleClick}
}

// 解决
function Solution1() {
  const handleClick = () => {
    console.log('点击');
  };
  
  return <button onClick={handleClick}>点击</button>;
}

11.2 问题2:this指向错误(类组件)

jsx
// 问题:类组件中this丢失
class Problem2 extends React.Component {
  handleClick() {
    console.log(this.state);  // this is undefined
  }
  
  render() {
    return <button onClick={this.handleClick}>点击</button>;
  }
}

// 解决方案1:箭头函数
class Solution2a extends React.Component {
  handleClick = () => {
    console.log(this.state);  // 正确
  }
  
  render() {
    return <button onClick={this.handleClick}>点击</button>;
  }
}

// 解决方案2:bind
class Solution2b extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
    console.log(this.state);
  }
  
  render() {
    return <button onClick={this.handleClick}>点击</button>;
  }
}

// 解决方案3:使用函数组件(推荐)
function Solution2c() {
  const handleClick = () => {
    console.log('点击');
  };
  
  return <button onClick={handleClick}>点击</button>;
}

11.3 问题3:无法访问事件对象属性

jsx
// 问题:React 16中异步访问事件
function Problem3() {
  const handleClick = (e) => {
    setTimeout(() => {
      console.log(e.target);  // React 16: null
    }, 0);
  };
  
  return <button onClick={handleClick}>点击</button>;
}

// 解决方案1:React 16中使用persist()
function Solution3a() {
  const handleClick = (e) => {
    e.persist();
    setTimeout(() => {
      console.log(e.target);  // 正确
    }, 0);
  };
  
  return <button onClick={handleClick}>点击</button>;
}

// 解决方案2:保存需要的属性
function Solution3b() {
  const handleClick = (e) => {
    const target = e.target;
    setTimeout(() => {
      console.log(target);  // 正确
    }, 0);
  };
  
  return <button onClick={handleClick}>点击</button>;
}

// 解决方案3:升级到React 17+(推荐)
function Solution3c() {
  const handleClick = (e) => {
    setTimeout(() => {
      console.log(e.target);  // React 17+: 正确
    }, 0);
  };
  
  return <button onClick={handleClick}>点击</button>;
}

11.4 问题4:stopPropagation不生效

jsx
// 问题:在原生事件中阻止不了React事件
function Problem4() {
  const buttonRef = useRef(null);
  
  useEffect(() => {
    const button = buttonRef.current;
    button.addEventListener('click', (e) => {
      e.stopPropagation();  // 只阻止原生事件传播
    });
  }, []);
  
  const handleReactClick = () => {
    console.log('React事件仍然触发');  // 仍然会执行
  };
  
  return <button ref={buttonRef} onClick={handleReactClick}>点击</button>;
}

// 解决:使用React事件或在捕获阶段阻止
function Solution4() {
  const handleReactClick = (e) => {
    e.stopPropagation();  // 使用React事件阻止
  };
  
  return <button onClick={handleReactClick}>点击</button>;
}

第十二部分:实战综合案例

12.1 拖拽排序列表

jsx
function DraggableList() {
  const [items, setItems] = useState([
    { id: 1, text: '项目1' },
    { id: 2, text: '项目2' },
    { id: 3, text: '项目3' }
  ]);
  
  const [draggingId, setDraggingId] = useState(null);
  const [overIndex, setOverIndex] = useState(null);
  
  const handleDragStart = (id) => (e) => {
    setDraggingId(id);
    e.dataTransfer.effectAllowed = 'move';
  };
  
  const handleDragOver = (index) => (e) => {
    e.preventDefault();
    setOverIndex(index);
  };
  
  const handleDrop = (dropIndex) => (e) => {
    e.preventDefault();
    
    if (draggingId === null) return;
    
    const dragIndex = items.findIndex(item => item.id === draggingId);
    const newItems = [...items];
    const [draggedItem] = newItems.splice(dragIndex, 1);
    newItems.splice(dropIndex, 0, draggedItem);
    
    setItems(newItems);
    setDraggingId(null);
    setOverIndex(null);
  };
  
  const handleDragEnd = () => {
    setDraggingId(null);
    setOverIndex(null);
  };
  
  return (
    <ul>
      {items.map((item, index) => (
        <li
          key={item.id}
          draggable
          onDragStart={handleDragStart(item.id)}
          onDragOver={handleDragOver(index)}
          onDrop={handleDrop(index)}
          onDragEnd={handleDragEnd}
          style={{
            padding: '10px',
            margin: '5px',
            background: draggingId === item.id ? '#e0e0e0' : 
                       overIndex === index ? '#f0f0f0' : 'white',
            border: '1px solid #ccc',
            cursor: 'move'
          }}
        >
          {item.text}
        </li>
      ))}
    </ul>
  );
}

12.2 图片裁剪工具

jsx
function ImageCropper({ imageSrc }) {
  const [crop, setCrop] = useState({ x: 0, y: 0, width: 100, height: 100 });
  const [dragging, setDragging] = useState(false);
  const [resizing, setResizing] = useState(false);
  const [startPos, setStartPos] = useState({ x: 0, y: 0 });
  
  const handleMouseDown = (type) => (e) => {
    if (type === 'move') {
      setDragging(true);
    } else {
      setResizing(true);
    }
    setStartPos({ x: e.clientX, y: e.clientY });
  };
  
  const handleMouseMove = (e) => {
    if (dragging) {
      const dx = e.clientX - startPos.x;
      const dy = e.clientY - startPos.y;
      setCrop(prev => ({
        ...prev,
        x: prev.x + dx,
        y: prev.y + dy
      }));
      setStartPos({ x: e.clientX, y: e.clientY });
    } else if (resizing) {
      const dx = e.clientX - startPos.x;
      const dy = e.clientY - startPos.y;
      setCrop(prev => ({
        ...prev,
        width: Math.max(50, prev.width + dx),
        height: Math.max(50, prev.height + dy)
      }));
      setStartPos({ x: e.clientX, y: e.clientY });
    }
  };
  
  const handleMouseUp = () => {
    setDragging(false);
    setResizing(false);
  };
  
  return (
    <div
      onMouseMove={handleMouseMove}
      onMouseUp={handleMouseUp}
      style={{ position: 'relative', width: '600px', height: '400px' }}
    >
      <img src={imageSrc} alt="裁剪" style={{ width: '100%', height: '100%' }} />
      
      <div
        style={{
          position: 'absolute',
          left: crop.x,
          top: crop.y,
          width: crop.width,
          height: crop.height,
          border: '2px dashed white',
          cursor: 'move'
        }}
        onMouseDown={handleMouseDown('move')}
      >
        <div
          style={{
            position: 'absolute',
            right: -5,
            bottom: -5,
            width: 10,
            height: 10,
            background: 'white',
            border: '1px solid black',
            cursor: 'nwse-resize'
          }}
          onMouseDown={handleMouseDown('resize')}
        />
      </div>
    </div>
  );
}

练习题

基础练习

  1. 创建组件显示事件对象的各个属性
  2. 实现参数传递的三种方式
  3. 测试事件的捕获和冒泡

进阶练习

  1. 实现一个图片裁剪工具
  2. 创建键盘导航的下拉菜单
  3. 实现拖拽排序列表

高级练习

  1. 分析React事件系统的源码
  2. 实现自定义的事件系统
  3. 优化大列表的事件处理性能

通过本章学习,你已经深入理解了React的事件对象和参数传递机制。这些知识是构建复杂交互的基础。继续学习,掌握更多React技能!