从一个栗子开始
实现一个组件,这个组件点击按钮会显示一个二维码,点击二维码之外的区域可以隐藏二维码,但是点击二维码本身却不会关闭
class Demo extends Component {
constructor(props) {
super(props);
this.state = {
active: false,
};
}
componentDidMount() {
document.body.addEventListener('click', e => {
this.setState({
active: false,
});
});
}
componentWillUnmount() {
document.body.removeEventListener('click');
}
handleClick = () => {
this.setState({
active: !this.state.active,
});
}
handleClickQr = (e) => {
e.stopPropagation();
}
render() {
return (
<div className="qr-wrapper">
<button className="qr" onClick={this.handleClick}>召唤二维码</button>
<div
className="code"
style={{ display: this.state.active ? 'block' : 'none' }}
onClick={this.handleClickQr}
>
<div style={{width: '100px', height: '100px', background: '#ff0050'}}>假装是二维码</div>
</div>
</div>
);
}
}
看上去确实可以实现要求,但事实上运行上述代码后可以发现,点击二维码本身也会导致二维码的隐藏。好神奇啊,这是为什么呢?
SyntheticEvent合成事件
React自身实现了一套自己的事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等,虽然和原生的是两码事,但也是基于浏览器的事件机制下完成的。
为什么自定义一套事件系统
- 抹平浏览器之间的兼容性差异: React根据W3C规范来定义这些合成事件
SyntheticEvent
,不会存在IE标准的兼容性问题,在各个浏览器之间达到统一,减少开发者的额外兼容工作。 - 跨平台 : 跟
Virtual DOM
一样,VirtualDOM
抽象了跨平台的渲染方式,对应的SyntheticEvent
目的也是想提供一个抽象的跨平台事件机制。 - 原生事件升级、改造:事件合成除了处理兼容性问题,还对原生事件做了升级改造,比较典型的是React的onChange事件,它为表单元素定义了统一的值变动事件,例如
blur
、change
、focus
、input
等。
基本流程
/**
* Summary of `ReactBrowserEventEmitter` event handling:
*
* - Top-level delegation is used to trap most native browser events. This
* may only occur in the main thread and is the responsibility of
* ReactDOMEventListener, which is injected and can therefore support
* pluggable event sources. This is the only work that occurs in the main
* thread.
*
* - We normalize and de-duplicate events to account for browser quirks. This
* may be done in the worker thread.
*
* - Forward these native events (with the associated top-level type used to
* trap it) to `EventPluginHub`, which in turn will ask plugins if they want
* to extract any synthetic events.
*
* - The `EventPluginHub` will then process each event by annotating them with
* "dispatches", a sequence of listeners and IDs that care about that event.
*
* - The `EventPluginHub` then dispatches the events.
*
* Overview of React and the event system:
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|EventPluginHub| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
* React Core . General Purpose Event Plugin System
*/
以上内容来自ReactBrowserEventEmitter.js
-
DOM - React通过事件委托机制将大部分(media)事件代理至Document层
-
ReactEventListener - 事件处理器. 在这里进行事件处理器的绑定。当DOM触发事件时,会从这里开始调度分发到React组件树
-
ReactEventEmitter - 暴露接口给React组件层用于添加事件订阅
-
EventPluginHub - 负责管理和注册各种插件。在事件分发时,调用插件来生成合成事件。 React事件系统使用了插件机制来管理不同行为的事件。这些插件会处理自己感兴趣的事件类型,并生成合成事件对象。目前ReactDOM有以下几种插件类型:
-
SimpleEventPlugin - 简单事件,处理一些比较通用的事件类型,同时对不同事件也做了分类。Discrete events(离散事件,如blur、focus、 click、 submit、 touchStart)、User-blocking events(用户阻塞事件,touchMove、mouseMove、scroll、drag、dragOver等)和Continuous events(可连续事件,如load、error、loadStart、abort、animationEnd等,优先级最高);
-
EnterLeaveEventPlugin - mouseEnter/mouseLeave和pointerEnter/pointerLeave这两类事件比较特殊, 和*over/*out事件相比,它们不支持事件冒泡,enter会给所有进入的元素发送事件, 行为有点类似于:hover;而over在进入元素后,还会冒泡通知其上级.
如果树层次比较深,大量的mouseenter触发可能导致性能问题。另外其不支持冒泡,无法在Document完美的监听和分发,所以ReactDOM使用*over/out事件来模拟这些enter/*leave。
-
ChangeEventPlugin - change事件是React的一个自定义事件,旨在规范化表单元素的变动事件。它支持这些表单元素: input, textarea, select
-
SelectEventPlugin - 和change事件一样,React为表单元素规范化了select(选择范围变动)事件,适用于input、textarea、contentEditable元素.
-
BeforeInputEventPlugin - beforeinput事件以及composition事件处理。
-
TapEventPlugin, 移动端IOS有个著名的300ms点击延迟,不过鉴于这个特性的修复、移除需要很长时间,所以React就不打算去支持了。关于这块的说明可看 react-tap-event-plugin
-
-
EventPropagators 按照DOM事件传播的两个阶段,遍历React组件树,并收集所有组件的事件处理器.
回到例子
由于大部分合成事件的代理注册的都是冒泡阶段的事件监听器,也就是委托到 document上注册的是冒泡阶段的事件监听器,所以就算显式声明了一个捕获阶段的React事件,例如onClickCapture,此事件的响应也会晚于原生事件的捕获事件以及冒泡事件。实际上,所有原生事件的响应(无论是冒泡事件还是捕获事件),都将早于React合成事件(SyntheticEvent),对原生事件调用e.stopPropagation()将阻止对应SyntheticEvent的响应,因为对应的事件根本无法到达document