写在前面
本文源于本人在学习react
过程中遇到的一个问题;本文内容为本人的一些的理解,如有不对的地方,还请大家指出来。本文是讲react的事件,不是介绍其api,而是猜想一下react
合成事件的实现方式
遇到的问题
class EventTest extends Component {handleParentClick(e) {console.log('click parent div');}handleChildClick(e) {e.stopPropagation();console.log('click child div');}componentDidMount() {document.querySelector('.parent').addEventListener('click', this.handleParentClick);}render() {return (<div className="parent"><div className="child" onClick={this.handleChildClick}></div></div>);}
}
上述代码render
出来后,尝试点击一下div.child
,诡异的现象产生了:
控制台中输出如上图所示,这完全不符合浏览器的事件执行啊,我所期望的是指输出click child div
,因为已经利用了e.stopPropagation()
来阻止冒泡,说明阻止冒泡失效了,但是仅仅如此吗,可以发现的是首先输出的是click parent div
(wtf)。
解决问题
为了解决上述问题,先来了解下react
的事件,react
事件是合成事件,为原生事件的一个子集,仅仅是进行了一个跨浏览器的封装。但是真的只有这么简单?图样图森破。
利用控制台,看下div.child
对应的事件处理函数:
一个空函数,事件的监听函数不是所定义的handleChildClick
,而是emptyFunction
,也就是说react
没有在真实的DOM
节点上绑定事件(在DOM
节点上绑定事件比较消耗内存,因为当dom
节点被remove
后,虽然不存在与dom tree
中,但是仍存在与内存中,需要手动remove
事件orchild = null
),react
的合成事件利用的是事件代理方式实现,也就是说会将事件监听器绑定到整个文档document
上,是不是这样呢?来验证一下,利用chrome:
可以发现,document
上的确被绑定了click
事件,dom
节点的真实的事件处理函数全部以一个特定的结构存储在了内存中,当点击div.child
时,这时其事件处理函数为emptyFunction
,执行这个函数无任何作用,按照浏览器标准事件模型,开始向上冒泡,这时到了div.parent
,于是输出了click parent div
,一直向上到了document
上,这时根据e.target
进行处理,而react
并不会根据dom
层级式传播那样遍历virtual dom
结构,这样有时遍历的层级会很多,而且会有很多的无效遍历。
react
是怎么做的呢?
react
依靠每个React component
各自独立的id
来编码这个层级。这样就能通过简单的字符串操作来获取所有父级 component 的父级内容,再把事件监听存储在hashmap当中,比如有如下结构并且为没一层div
添加onClick
div.adiv.bdiv.c
当点击div.c
时,处理方式:
clickBubbleListeners['a.b.c'](event);
clickBubbleListeners['a.b'](event);
clickBubbleListeners['a'](event);
在合成事件中用e.stopPropagation
只能阻断上述冒泡过程。
结论
由此可以看出:
阻止
react
事件冒泡的行为只能用于react
合成事件中,对于原生事件无效(合成事件中的e.stopPropagation
与原生事件中的e.stopPropagation
并不是一回事)阻止原生事件的冒泡行为,可以阻止
react
合成事件的传播(根本不会冒泡到document
上,所以不会触发react
的合成事件)在写
react
时,最好不要将合成事件与原生事件混用。
参考
本文部分参考自IMWeb—React事件初探