Skip to content

事件委托机制 - React事件处理优化策略

1. 事件委托基础

1.1 什么是事件委托

typescript
const eventDelegationConcept = {
  definition: '将事件监听器绑定到父元素,通过事件冒泡处理子元素事件',
  
  原理: {
    事件冒泡: '子元素事件会冒泡到父元素',
    事件目标: '通过event.target识别实际触发元素',
    统一处理: '父元素统一处理所有子元素事件'
  },
  
  优势: [
    '减少内存占用',
    '动态元素无需重新绑定',
    '简化事件管理',
    '提升性能'
  ],
  
  应用场景: [
    '列表项点击',
    '动态添加的元素',
    '表单元素',
    '菜单导航'
  ]
};

1.2 原生JavaScript事件委托

javascript
// 传统方式: 每个元素绑定事件
const badApproach = {
  html: `
    <ul id="list">
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </ul>
  `,
  
  code: `
    // ❌ 不好: 为每个li绑定事件
    const items = document.querySelectorAll('#list li');
    items.forEach(item => {
      item.addEventListener('click', function(e) {
        console.log('Clicked:', this.textContent);
      });
    });
    
    // 问题:
    // 1. 创建了3个事件监听器
    // 2. 新增li需要重新绑定
    // 3. 内存占用大
  `
};

// 事件委托方式
const goodApproach = {
  code: `
    // ✓ 好: 事件委托
    const list = document.getElementById('list');
    list.addEventListener('click', function(e) {
      // 检查点击的是否是li
      if (e.target.tagName === 'LI') {
        console.log('Clicked:', e.target.textContent);
      }
    });
    
    // 优势:
    // 1. 只有1个事件监听器
    // 2. 动态添加的li自动支持
    // 3. 内存占用小
    
    // 动态添加元素
    const newItem = document.createElement('li');
    newItem.textContent = 'Item 4';
    list.appendChild(newItem);
    // 新元素自动支持点击事件
  `
};

1.3 事件冒泡和捕获

javascript
// 事件传播三个阶段
const eventPropagation = {
  phase1_捕获: {
    description: '从window到目标元素',
    direction: 'window -> document -> html -> ... -> target',
    useCapture: true
  },
  
  phase2_目标: {
    description: '到达目标元素',
    order: '按注册顺序执行'
  },
  
  phase3_冒泡: {
    description: '从目标元素到window',
    direction: 'target -> ... -> html -> document -> window',
    useCapture: false
  }
};

// 示例
const propagationExample = `
  <div id="outer">
    <div id="middle">
      <div id="inner">Click Me</div>
    </div>
  </div>
  
  <script>
    // 冒泡阶段(默认)
    outer.addEventListener('click', () => console.log('Outer'));
    middle.addEventListener('click', () => console.log('Middle'));
    inner.addEventListener('click', () => console.log('Inner'));
    
    // 点击Inner输出: Inner -> Middle -> Outer
    
    // 捕获阶段
    outer.addEventListener('click', () => console.log('Outer Capture'), true);
    middle.addEventListener('click', () => console.log('Middle Capture'), true);
    inner.addEventListener('click', () => console.log('Inner Capture'), true);
    
    // 点击Inner输出:
    // Outer Capture -> Middle Capture -> Inner Capture
    // -> Inner -> Middle -> Outer
  </script>
`;

2. React事件委托演进

2.1 React 16及之前

typescript
// React 16: 事件委托到document
const react16Delegation = {
  机制: `
    1. 所有事件注册到document
    2. 原生事件冒泡到document
    3. React触发合成事件系统
    4. 收集路径上的所有监听器
    5. 执行监听器
  `,
  
  问题: [
    '与第三方库冲突',
    '多React实例互相干扰',
    'stopPropagation不能阻止原生事件',
    '门户(Portal)事件冒泡不符合直觉'
  ],
  
  代码示例: `
    // React 16
    ReactDOM.render(<App />, container);
    
    // 所有事件监听器在document上
    document.addEventListener('click', reactClickHandler);
    document.addEventListener('change', reactChangeHandler);
    // ...
  `
};

// 与第三方库的冲突
const react16Conflict = `
  // 第三方库在document上监听
  document.addEventListener('click', function(e) {
    console.log('Third party');
    e.stopPropagation(); // 阻止冒泡
  });
  
  // React组件
  function App() {
    const handleClick = () => {
      console.log('React click'); // 不会执行!
    };
    
    return <button onClick={handleClick}>Click</button>;
  }
  
  // 问题: 第三方库阻止了冒泡,React事件无法触发
`;

2.2 React 17+改进

typescript
// React 17: 事件委托到根容器
const react17Delegation = {
  改进: `
    1. 事件注册到root容器(而非document)
    2. 每个React应用独立
    3. 更好的与第三方库集成
    4. Portal事件冒泡更自然
  `,
  
  优势: [
    '多React应用共存',
    '渐进式升级',
    '减少冲突',
    '更符合DOM规范'
  ],
  
  代码示例: `
    // React 17+
    const root = ReactDOM.createRoot(container);
    root.render(<App />);
    
    // 事件监听器在container上
    container.addEventListener('click', reactClickHandler);
    container.addEventListener('change', reactChangeHandler);
    // ...
  `,
  
  解决冲突: `
    // 现在不会冲突
    document.addEventListener('click', function(e) {
      console.log('Third party');
      e.stopPropagation();
    });
    
    function App() {
      const handleClick = () => {
        console.log('React click'); // 正常执行!
      };
      
      return <button onClick={handleClick}>Click</button>;
    }
  `
};

2.3 对比分析

typescript
const delegationComparison = {
  React16: {
    委托位置: 'document',
    作用域: '全局',
    多应用: '互相干扰',
    第三方库: '容易冲突',
    Portal: '冒泡不自然',
    升级: '全量升级'
  },
  
  React17: {
    委托位置: 'root容器',
    作用域: '应用隔离',
    多应用: '互不干扰',
    第三方库: '减少冲突',
    Portal: '符合DOM规范',
    升级: '渐进式升级'
  }
};

3. React事件委托实现

3.1 事件注册

typescript
// React 17+ 事件注册流程
function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  // 标记已注册
  if ((rootContainerElement as any)[rootListenersMarkerKey]) {
    return;
  }
  (rootContainerElement as any)[rootListenersMarkerKey] = true;
  
  // 注册所有支持的事件
  allNativeEvents.forEach(domEventName => {
    // 大部分事件使用委托
    if (!nonDelegatedEvents.has(domEventName)) {
      listenToNativeEvent(
        domEventName,
        false, // 冒泡阶段
        rootContainerElement
      );
    }
    
    // 同时注册捕获阶段
    listenToNativeEvent(
      domEventName,
      true,  // 捕获阶段
      rootContainerElement
    );
  });
  
  // 特殊事件不使用委托
  const ownerDocument = 
    rootContainerElement.nodeType === DOCUMENT_NODE
      ? rootContainerElement
      : rootContainerElement.ownerDocument;
  
  if (ownerDocument !== null) {
    nonDelegatedEvents.forEach(domEventName => {
      listenToNativeEvent(
        domEventName,
        false,
        ownerDocument as EventTarget
      );
    });
  }
}

// 不使用委托的事件
const nonDelegatedEvents = new Set([
  'cancel',
  'close',
  'invalid',
  'load',
  'scroll',
  'toggle',
  'error',
  'abort',
  'canplay',
  'canplaythrough',
  'durationchange',
  'emptied',
  'encrypted',
  'ended',
  'loadeddata',
  'loadedmetadata',
  'loadstart',
  'pause',
  'play',
  'playing',
  'progress',
  'ratechange',
  'seeked',
  'seeking',
  'stalled',
  'suspend',
  'timeupdate',
  'volumechange',
  'waiting'
]);

3.2 事件监听器创建

typescript
function listenToNativeEvent(
  domEventName: string,
  isCapturePhaseListener: boolean,
  target: EventTarget
): void {
  let eventSystemFlags = 0;
  
  if (isCapturePhaseListener) {
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }
  
  addTrappedEventListener(
    target,
    domEventName,
    eventSystemFlags,
    isCapturePhaseListener
  );
}

function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: string,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean
): void {
  // 根据优先级创建监听器
  const listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags
  );
  
  let unsubscribeListener;
  
  // 添加事件监听
  if (isCapturePhaseListener) {
    unsubscribeListener = addEventCaptureListener(
      targetContainer,
      domEventName,
      listener
    );
  } else {
    unsubscribeListener = addEventBubbleListener(
      targetContainer,
      domEventName,
      listener
    );
  }
}

function addEventBubbleListener(
  target: EventTarget,
  eventType: string,
  listener: Function
): () => void {
  target.addEventListener(eventType, listener as any, false);
  return () => {
    target.removeEventListener(eventType, listener as any, false);
  };
}

function addEventCaptureListener(
  target: EventTarget,
  eventType: string,
  listener: Function
): () => void {
  target.addEventListener(eventType, listener as any, true);
  return () => {
    target.removeEventListener(eventType, listener as any, true);
  };
}

3.3 事件分发

typescript
// 事件分发入口
function dispatchEvent(
  domEventName: string,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: Event
): void {
  // 获取事件目标
  const nativeEventTarget = getEventTarget(nativeEvent);
  
  // 找到对应的Fiber节点
  const targetInst = getClosestInstanceFromNode(nativeEventTarget);
  
  // 批量更新
  batchedEventUpdates(() =>
    dispatchEventsForPlugins(
      domEventName,
      eventSystemFlags,
      nativeEvent,
      targetInst,
      targetContainer
    )
  );
}

// 获取事件目标
function getEventTarget(nativeEvent: Event): EventTarget {
  let target = nativeEvent.target || nativeEvent.srcElement || window;
  
  // Safari可能返回文本节点
  if ((target as any).nodeType === TEXT_NODE) {
    target = (target as any).parentNode;
  }
  
  return target;
}

// 从DOM节点获取最近的Fiber实例
function getClosestInstanceFromNode(targetNode: Node): Fiber | null {
  const targetInst = (targetNode as any)[internalInstanceKey];
  
  if (targetInst) {
    return targetInst;
  }
  
  // 向上查找
  let node = targetNode;
  while (node) {
    const inst = (node as any)[internalInstanceKey];
    if (inst) {
      return inst;
    }
    node = node.parentNode;
  }
  
  return null;
}

3.4 收集事件监听器

typescript
// 收集单阶段监听器(冒泡或捕获)
function accumulateSinglePhaseListeners(
  targetFiber: Fiber | null,
  reactName: string,
  nativeEventType: string,
  inCapturePhase: boolean
): Array<DispatchListener> {
  const captureName = reactName !== null ? reactName + 'Capture' : null;
  const reactEventName = inCapturePhase ? captureName : reactName;
  const listeners: Array<DispatchListener> = [];
  
  let instance = targetFiber;
  let lastHostComponent: Fiber | null = null;
  
  // 从目标向上遍历Fiber树
  while (instance !== null) {
    const { stateNode, tag } = instance;
    
    // 只处理HostComponent(原生DOM元素)
    if (tag === HostComponent && stateNode !== null) {
      lastHostComponent = instance;
      const currentTarget = stateNode;
      
      // 获取监听器
      if (reactEventName !== null) {
        const listener = getListener(instance, reactEventName);
        
        if (listener != null) {
          listeners.push(
            createDispatchListener(
              instance,
              listener,
              currentTarget
            )
          );
        }
      }
    }
    
    instance = instance.return;
  }
  
  return listeners;
}

// 创建分发监听器
function createDispatchListener(
  instance: Fiber,
  listener: Function,
  currentTarget: EventTarget
): DispatchListener {
  return {
    instance,
    listener,
    currentTarget
  };
}

// 从Fiber获取事件监听器
function getListener(inst: Fiber, registrationName: string): Function | null {
  const { stateNode } = inst;
  
  if (stateNode === null) {
    return null;
  }
  
  const props = getFiberCurrentPropsFromNode(stateNode);
  
  if (props === null) {
    return null;
  }
  
  const listener = props[registrationName];
  
  return listener || null;
}

4. 特殊事件处理

4.1 不冒泡的事件

typescript
// Focus和Blur事件
const focusBlurDelegation = {
  问题: 'focus和blur不冒泡',
  
  解决方案: '使用focusin和focusout(会冒泡)',
  
  实现: `
    // React内部转换
    if (domEventName === 'focusin') {
      registerSimpleEvent('onFocus', 'focusin');
    }
    if (domEventName === 'focusout') {
      registerSimpleEvent('onBlur', 'focusout');
    }
  `,
  
  兼容性: '所有现代浏览器都支持focusin/focusout'
};

// MouseEnter和MouseLeave
const mouseEnterLeaveDelegation = {
  问题: 'mouseenter和mouseleave不冒泡',
  
  解决方案: '使用mouseover和mouseout模拟',
  
  实现: `
    function extractMouseEnterLeaveEvent(
      dispatchQueue,
      domEventName,
      targetInst,
      nativeEvent,
      nativeEventTarget
    ) {
      const isOverEvent = domEventName === 'mouseover';
      const isOutEvent = domEventName === 'mouseout';
      
      if (!isOverEvent && !isOutEvent) {
        return;
      }
      
      // 获取相关目标
      const related = isOverEvent 
        ? nativeEvent.relatedTarget 
        : nativeEventTarget;
      
      // 检查是否真的进入/离开
      if (contains(targetInst.stateNode, related)) {
        return; // 没有真正进入/离开
      }
      
      // 创建mouseenter/mouseleave事件
      const syntheticEvent = createMouseEvent(
        isOverEvent ? 'mouseenter' : 'mouseleave',
        targetInst,
        nativeEvent,
        nativeEventTarget
      );
      
      dispatchQueue.push({
        event: syntheticEvent,
        listeners: accumulateEnterLeaveListeners(targetInst, related)
      });
    }
  `
};

// Scroll事件
const scrollDelegation = {
  问题: 'scroll在某些元素上不冒泡',
  
  React17变化: 'onScroll不再冒泡',
  
  原因: '更符合DOM规范',
  
  影响: `
    // React 16: scroll会冒泡
    <div onScroll={handleParentScroll}>
      <div onScroll={handleChildScroll}>
        {/* handleParentScroll会被触发 */}
      </div>
    </div>
    
    // React 17+: scroll不冒泡
    <div onScroll={handleParentScroll}>
      <div onScroll={handleChildScroll}>
        {/* handleParentScroll不会被触发 */}
      </div>
    </div>
  `
};

4.2 媒体事件

typescript
// 媒体事件不使用委托
const mediaEventDelegation = {
  不委托的原因: [
    '媒体事件大多不冒泡',
    '通常绑定到特定元素',
    '频率低,性能影响小'
  ],
  
  直接绑定: `
    // 这些事件直接绑定到元素
    <video
      onPlay={handlePlay}
      onPause={handlePause}
      onEnded={handleEnded}
      onError={handleError}
    />
  `,
  
  事件列表: [
    'play', 'pause', 'playing', 'ended',
    'canplay', 'canplaythrough',
    'loadeddata', 'loadedmetadata',
    'timeupdate', 'volumechange',
    'seeking', 'seeked',
    'waiting', 'stalled'
  ]
};

5. Portal中的事件委托

5.1 Portal事件冒泡

typescript
// Portal定义
const portalConcept = `
  // Portal可以将子节点渲染到DOM的不同位置
  ReactDOM.createPortal(
    child,
    container
  )
`;

// React 16的问题
const react16PortalIssue = {
  问题: `
    Portal的DOM在document下的不同位置,
    但事件冒泡遵循React组件树
  `,
  
  示例: `
    function App() {
      return (
        <div onClick={() => console.log('App')}>
          <Modal />
        </div>
      );
    }
    
    function Modal() {
      return ReactDOM.createPortal(
        <div onClick={() => console.log('Modal')}>
          Click Me
        </div>,
        document.body
      );
    }
    
    // DOM结构:
    <div id="root">
      <div> <!-- App的div -->
        <!-- Modal的div不在这里 -->
      </div>
    </div>
    <div> <!-- Modal的div在body下 -->
      Click Me
    </div>
    
    // React 16: 点击Modal会触发App的onClick
    // 原因: 事件在React组件树中冒泡,不是DOM树
  `,
  
  困惑: 'DOM结构和事件冒泡路径不一致'
};

// React 17的改进
const react17PortalImprovement = {
  改进: '事件委托到root,Portal事件冒泡更自然',
  
  行为: `
    React 17+:
    - Portal内的事件先在Portal容器处理
    - 然后才冒泡到root容器
    - 更符合DOM事件冒泡规范
  `,
  
  示例: `
    function App() {
      return (
        <div onClick={() => console.log('App')}>
          <Modal />
        </div>
      );
    }
    
    function Modal() {
      return ReactDOM.createPortal(
        <div onClick={() => console.log('Modal')}>
          Click Me
        </div>,
        document.body
      );
    }
    
    // React 17+: 点击Modal不会触发App的onClick
    // 因为事件在body上,不会冒泡到root容器
  `
};

5.2 Portal事件实现

typescript
// Portal事件处理
function accumulatePortalListeners(
  instance: Fiber,
  reactName: string,
  listeners: Array<DispatchListener>
): void {
  // 检查是否是Portal
  while (instance !== null) {
    if (instance.tag === HostPortal) {
      const portalContainer = instance.stateNode.containerInfo;
      
      // 在Portal容器上收集监听器
      const portalInst = getClosestInstanceFromNode(portalContainer);
      if (portalInst !== null) {
        accumulateSinglePhaseListeners(
          portalInst,
          reactName,
          /* nativeEventType */ '',
          /* inCapturePhase */ false
        );
      }
    }
    
    instance = instance.return;
  }
}

6. 性能优化

6.1 减少监听器数量

typescript
// 对比: 传统方式 vs 事件委托
const performanceComparison = {
  传统方式: {
    代码: `
      // 1000个按钮,1000个监听器
      function TodoList({ items }) {
        return (
          <ul>
            {items.map(item => (
              <li key={item.id}>
                <button onClick={() => handleDelete(item.id)}>
                  Delete
                </button>
              </li>
            ))}
          </ul>
        );
      }
    `,
    监听器: '1000个',
    内存: '高',
    性能: '差'
  },
  
  事件委托: {
    代码: `
      // React自动事件委托,只有1个监听器在root
      function TodoList({ items }) {
        return (
          <ul>
            {items.map(item => (
              <li key={item.id}>
                <button onClick={() => handleDelete(item.id)}>
                  Delete
                </button>
              </li>
            ))}
          </ul>
        );
      }
    `,
    监听器: '1个(在root)',
    内存: '低',
    性能: '优'
  }
};

6.2 动态元素支持

typescript
// 事件委托自动支持动态元素
const dynamicElementSupport = `
  function TodoList() {
    const [items, setItems] = useState([]);
    
    const addItem = () => {
      setItems([...items, { id: Date.now(), text: 'New Item' }]);
      // 新元素自动支持点击事件,无需重新绑定
    };
    
    const handleItemClick = (id) => {
      console.log('Clicked:', id);
    };
    
    return (
      <div>
        <button onClick={addItem}>Add Item</button>
        <ul>
          {items.map(item => (
            <li key={item.id} onClick={() => handleItemClick(item.id)}>
              {item.text}
            </li>
          ))}
        </ul>
      </div>
    );
  }
`;

6.3 避免过度委托

typescript
// 注意事项
const delegationCaveats = {
  过度委托问题: {
    场景: '大型列表,频繁事件',
    问题: '每次事件都需要向上查找',
    示例: `
      // 10000个元素,频繁mousemove
      <div>
        {items.map(item => (
          <div onMouseMove={handleMove}>
            {/* 每次移动都要遍历查找 */}
          </div>
        ))}
      </div>
    `,
    优化: `
      // 使用虚拟滚动减少DOM数量
      import { FixedSizeList } from 'react-window';
      
      <FixedSizeList
        height={400}
        itemCount={items.length}
        itemSize={35}
      >
        {({ index, style }) => (
          <div style={style} onMouseMove={handleMove}>
            {items[index]}
          </div>
        )}
      </FixedSizeList>
    `
  },
  
  阻止冒泡的影响: {
    问题: 'stopPropagation会阻止委托',
    示例: `
      function Parent() {
        const handleParentClick = () => {
          console.log('Parent'); // 不会执行
        };
        
        return (
          <div onClick={handleParentClick}>
            <Child />
          </div>
        );
      }
      
      function Child() {
        const handleChildClick = (e) => {
          e.stopPropagation(); // 阻止冒泡
          console.log('Child');
        };
        
        return <button onClick={handleChildClick}>Click</button>;
      }
    `,
    建议: '谨慎使用stopPropagation'
  }
};

7. 与第三方库集成

7.1 jQuery冲突处理

typescript
// React与jQuery共存
const jQueryIntegration = {
  React16问题: `
    // jQuery阻止了事件冒泡到document
    $(document).on('click', '.button', function(e) {
      e.stopPropagation();
      // React事件不会触发
    });
  `,
  
  React17解决: `
    // React 17事件在root,不受影响
    $(document).on('click', '.button', function(e) {
      e.stopPropagation();
      // React事件正常触发
    });
  `,
  
  最佳实践: `
    // 使用命名空间避免冲突
    $(document).on('click.myapp', '.button', function(e) {
      // ...
    });
    
    // 清理
    $(document).off('click.myapp');
  `
};

7.2 原生事件监听

typescript
// 在React组件中添加原生监听
const nativeEventListeners = {
  问题: '与React事件委托的关系',
  
  示例: `
    function Component() {
      const ref = useRef(null);
      
      useEffect(() => {
        const element = ref.current;
        
        // 原生事件监听
        const handleClick = (e) => {
          console.log('Native click');
        };
        
        element.addEventListener('click', handleClick);
        
        return () => {
          element.removeEventListener('click', handleClick);
        };
      }, []);
      
      // React事件
      const handleReactClick = () => {
        console.log('React click');
      };
      
      return (
        <div ref={ref} onClick={handleReactClick}>
          Click Me
        </div>
      );
    }
    
    // 执行顺序:
    // 1. 原生事件(在元素上)
    // 2. React事件(在root)
  `,
  
  注意: [
    '原生事件先于React事件',
    '原生stopPropagation会阻止React事件',
    '清理监听器避免内存泄漏'
  ]
};

8. 面试高频问题

typescript
const delegationInterviewQA = {
  Q1: {
    question: '什么是事件委托?为什么使用?',
    answer: [
      '定义: 将事件监听器绑定到父元素,利用事件冒泡处理子元素事件',
      '优势:',
      '  - 减少内存占用(少量监听器)',
      '  - 支持动态元素',
      '  - 简化事件管理',
      '  - 提升性能'
    ]
  },
  
  Q2: {
    question: 'React事件委托机制?',
    answer: `
      React 17+:
      1. 所有事件注册到root容器
      2. 原生事件冒泡到root
      3. React触发合成事件系统
      4. 从event.target向上收集监听器
      5. 按顺序执行监听器
      
      特点:
      - 每个React应用独立
      - 减少与第三方库冲突
      - Portal事件冒泡更自然
    `
  },
  
  Q3: {
    question: 'React 17事件系统的改进?',
    answer: [
      '1. 从document改为root容器',
      '2. 多React应用互不干扰',
      '3. 减少与第三方库冲突',
      '4. Portal事件冒泡更符合DOM规范',
      '5. 支持渐进式升级',
      '6. onScroll不再冒泡'
    ]
  },
  
  Q4: {
    question: '哪些事件不使用委托?',
    answer: `
      不冒泡的事件:
      - 媒体事件(play, pause等)
      - scroll
      - load, error
      - focus, blur(使用focusin/out替代)
      
      原因:
      - 这些事件不冒泡或冒泡行为特殊
      - 直接绑定到元素更合适
    `
  },
  
  Q5: {
    question: '事件委托的性能影响?',
    answer: [
      '优势:',
      '  - 减少监听器数量',
      '  - 降低内存占用',
      '  - 支持动态元素',
      '劣势:',
      '  - 每次事件需要查找目标',
      '  - 大列表+高频事件可能影响性能',
      '优化:',
      '  - 使用虚拟滚动',
      '  - 避免过深的DOM树'
    ]
  },
  
  Q6: {
    question: 'stopPropagation在React中的影响?',
    answer: `
      在React事件中:
      - 阻止React合成事件冒泡
      - 不阻止原生事件冒泡
      
      如果需要阻止原生事件:
      e.nativeEvent.stopImmediatePropagation()
      
      注意:
      - 谨慎使用stopPropagation
      - 可能影响事件委托
      - 可能破坏第三方库
    `
  }
};

9. 最佳实践

typescript
const delegationBestPractices = {
  利用委托优势: {
    动态列表: `
      // ✓ 好: 自动支持动态元素
      function List({ items }) {
        return (
          <ul>
            {items.map(item => (
              <li key={item.id} onClick={() => handle(item)}>
                {item.text}
              </li>
            ))}
          </ul>
        );
      }
    `,
    
    避免: `
      // ❌ 不好: 手动管理事件监听
      function List({ items }) {
        useEffect(() => {
          items.forEach(item => {
            const el = document.getElementById(item.id);
            el?.addEventListener('click', () => handle(item));
          });
        }, [items]);
        
        // ...
      }
    `
  },
  
  性能优化: {
    虚拟滚动: `
      // 大列表使用虚拟滚动
      import { FixedSizeList } from 'react-window';
      
      <FixedSizeList
        height={400}
        itemCount={10000}
        itemSize={35}
      >
        {Row}
      </FixedSizeList>
    `,
    
    事件节流: `
      // 高频事件使用节流
      const handleScroll = useCallback(
        throttle(() => {
          // 处理滚动
        }, 100),
        []
      );
      
      <div onScroll={handleScroll} />
    `
  },
  
  避免冲突: {
    命名空间: `
      // 使用唯一的className或data属性
      <button 
        className="my-app-button"
        data-action="delete"
        onClick={handleClick}
      >
        Delete
      </button>
    `,
    
    原生事件: `
      // 原生事件监听记得清理
      useEffect(() => {
        const handler = () => {};
        element.addEventListener('click', handler);
        
        return () => {
          element.removeEventListener('click', handler);
        };
      }, []);
    `
  }
};

10. 总结

事件委托机制的核心要点:

  1. 原理: 利用事件冒泡,父元素统一处理
  2. React实现: 委托到root容器
  3. React 17改进: 从document改为root
  4. 优势: 减少监听器,支持动态元素
  5. 特殊事件: 不冒泡的事件特殊处理
  6. Portal: 事件冒泡遵循React树
  7. 性能: 大部分场景性能更优
  8. 第三方库: React 17减少冲突

理解事件委托是掌握React事件系统的关键。