合成事件

SyntheticEvent | 合成事件详解

Event pooling

如上图所示,在 JavaScript 中,事件的触发实质上是要经过三个阶段:事件捕获、目标对象本身的事件处理和事件冒泡,假设在 div 中触发了 click 事件,实际上首先经历捕获阶段会由父级元素将事件一直传递到事件发生的元素,执行完目标事件本身的处理事件后,然后经历冒泡阶段,将事件从子元素向父元素冒泡。正因为事件在 DOM 的传递经历这样一个过程,从而为行为委托提供了可能。通俗地讲,行为委托的实质就是将子元素事件的处理委托给父级元素处理。

React 会将所有的事件都绑定在最外层(document),使用统一的事件监听,并在冒泡阶段处理事件,当挂载或者卸载组件时,只需要在通过的在统一的事件监听位置增加或者删除对象,因此可以提高效率。并且 React 并没有使用原生的浏览器事件,而是在基于 Virtual DOM 的基础上实现了合成事件(SyntheticEvent),事件处理程序接收到的是 SyntheticEvent 的实例。SyntheticEvent 完全符合 W3C 的标准,因此在事件层次上具有浏览器兼容性,与原生的浏览器事件一样拥有同样的接口,可以通过 stopPropagation()和 preventDefault() 相应的中断。如果需要访问当原生的事件对象,可以通过引用 nativeEvent 获得。

上图为大致的 React 事件机制的流程图,React 中的事件机制分为两个阶段:事件注册和事件触发:

  • 事件注册:React 在组件加载(mount)和更新(update)时,其中的 ReactDOMComponent 会对传入的事件属性进行处理,对相关事件进行注册和存储。document 中注册的事件不处理具体的事件,仅对事件进行分发。ReactBrowserEventEmitter 作为事件注册入口,担负着事件注册和事件触发。注册事件的回调函数由 EventPluginHub 来统一管理,根据事件的类型(type)和组件标识(_rootNodeID)为 key 唯一标识事件并进行存储。

  • 事件执行:事件执行时,document 上绑定事件 ReactEventListener.dispatchEvent 会对事件进行分发,根据之前存储的类型(type)和组件标识(_rootNodeID)找到触发事件的组件。ReactEventEmitter 利用 EventPluginHub 中注入(inject)的 plugins(例如:SimpleEventPlugin、EnterLeaveEventPlugin)会将原生的 DOM 事件转化成合成的事件,然后批量执行存储的回调函,回调函数的执行分为两步,第一步是将所有的合成事件放到事件队列里面,第二步是逐个执行。需要注意的是,浏览器原生会为每个事件的每个 listener 创建一个事件对象,可以从这个事件对象获取到事件的引用。这会造成高额的内存分配,React 在启动时就会为每种对象分配内存池,用到某一个事件对象时就可以从这个内存池进行复用,节省内存。

外部事件触发

外部触发关闭

点击事件是 Web 开发中常见的事件之一,我们在上文中介绍的基本事件绑定也是以点击事件为例。这里我们想讨论下另一个常见的与点击相关的需求,即点击组件外部分触发事件处理。典型的用例譬如在弹出窗中,我们希望点击弹出窗外的部分自动关闭弹出窗,或者某个下拉菜单打开状态下,点击其他部分自动关闭该菜单。这种需求有一种解决思路是为组件设置一个全局浮层,这样可以将组件外的点击事件绑定到浮层上,直接监听浮层的点击即可。不过很多产品经理在提需求的时候是不喜欢这种方式的,因此我们可以使用另一种方法,直接在 React 根容器中监听点击事件:

window.__myapp_container = document.getElementById("app");

React.render(<App />, window.__myapp_container);

然后在组件中我们动态的设置对于根元素的监听:

export default class ClickListener extends Component {
  static propTypes = {
    children: PropTypes.node.isRequired,
    onClickOutside: PropTypes.func.isRequired
  };

  componentDidMount() {
    window.__myapp_container.addEventListener(
      "click",
      this.handleDocumentClick
    );
  }

  componentWillUnmount() {
    window.__myapp_container.removeEventListener(
      "click",
      this.handleDocumentClick
    );
  } /* using fat arrow to bind to instance */

  handleDocumentClick = evt => {
    const area = ReactDOM.findDOMNode(this.refs.area);

    if (!area.contains(evt.target)) {
      this.props.onClickOutside(evt);
    }
  };

  render() {
    return (
      <div ref="area">
             {this.props.children}
            
      </div>
    );
  }
}

自定义事件分发

下一页