最近在阅读《深刻React技术栈》一书中,发现了以前使用React中并无注意到的React事件与浏览器原生事件之间的区别,鉴于很久已经没有写东西了,就想写一下关于React事件的文章。
首先咱们举个例子,若是咱们须要实现一个组件,这个组件点击按钮会显示一个二维码,点击二维码以外的区域能够隐藏二维码,可是点击二维码自己却不会关闭,代码以下:javascript
//代码来源于《深刻React技术栈》2.1.4节 class QrCode extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); this.handleClickQr = this.handleClickQr.bind(this); 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} > <img src="qr.jpg" alt="qr" /> </div> </div> ); } }
上面代码从感官上感受确实能够实现要求的组件,但事实上咱们运行上述代码能够发现,点击二维码自己也会致使二维码的隐藏,如今就有意思了,咱们来仔细分析一下。
其实React事件并无原生的绑定在真实的DOM上,而是使用了行为委托方式实现事件机制。
java
如上图所示,在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
惟一标识事件并进行存储。app
事件执行
事件执行时,document上绑定事件ReactEventListener.dispatchEvent
会对事件进行分发,根据以前存储的类型(type
)和组件标识(_rootNodeID
)找到触发事件的组件。ReactEventEmitter
利用EventPluginHub
中注入(inject
)的plugins
(例如:SimpleEventPlugin
、EnterLeaveEventPlugin
)会将原生的DOM事件转化成合成的事件,而后批量执行存储的回调函,回调函数的执行分为两步,第一步是将全部的合成事件放到事件队列里面,第二步是逐个执行。须要注意的是,浏览器原生会为每一个事件的每一个listener建立一个事件对象,能够从这个事件对象获取到事件的引用。这会形成高额的内存分配,React在启动时就会为每种对象分配内存池,用到某一个事件对象时就能够从这个内存池进行复用,节省内存。函数
再回到咱们刚开始的问题,如今看起来就很没有很费解了,之因此会出现上面的问题是由于咱们混用了React的事件机制和DOM原生的事件机制,认为经过:this
handleClickQr(e) { e.stopPropagation(); }
就能阻止原生的事件传播,其实在事件委托的情形下是不能实现这一点的。固然解决的办法也不复杂,不要将React事件和DOM原生事件混用。spa
componentDidMount() { document.body.addEventListener('click', e => { this.setState({ active: false, }); }); document.querySelector('.code').addEventListener('click', e => { e.stopPropagation(); }) } componentWillUnmount() { document.body.removeEventListener('click'); document.querySelector('.qr').removeEventListener('click'); }
或者经过事件原件对象中的target
进行判断:code
componentDidMount() { document.body.addEventListener('click', e => { if (e.target && e.target.matches('div.code')) { return; } this.setState({ active: false, }); }); }
均可以解决异常关闭的问题。component