在上一篇《从Chrome源码看浏览器如何构建DOM树》介绍了blink如何创建一棵DOM树,在这一篇将介绍事件机制。
上一篇还有一个地方未提及,那就是在构建完DOM之后,浏览器将会触发DOMContentLoaded事件,这个事件是在处理tokens的时候遇到EndOfFile标志符时触发的:
上面代码第1行,遇到结尾的token时,将会在第6行停止解析。这是最后一个待处理的token,一般是跟在</html>
后面的一个\\EOF
标志符来的。
第6行的prepareToStopParsing,会在Document的finishedParseing里面生成一个事件,再调用dispatchEvent,进一步调用监听函数:
这个dispatchEvent是EventTarget这个类的成员函数。在上一篇描述DOM的结点数据结构时将Node作为根结点,其实Node上面还有一个类,就是EventTarget。我们先来看一下事件的数据结构是怎么样的:
1. 事件的数据结构
画出事件相关的类图:
在最顶层的EventTarget提供了三个函数,分别是添加监听add、删除监听remove、触发监听fire。一个典型的访问者模式我在《Effective前端5:减少前端代码耦合》提到了,这里重点看一下blink实际上是怎么实现的。
在Node类组合了一个EventTargetDataMap,这是一个哈希map,并且它是静态成员变量。它的key值是当前结点Node实例的指针,value值是事件名称和对应的listeners。如果画一个示例图,它的存储是这样的:
如上,按照正常的思维,存放事件名称和对应的访问者应该是用一个哈希map,但是blink却是用的向量vector + pair,这就导致在查找某个事件的访问者的时候,需要循环所有已添加的事件名称依次比较字符串值是否相等。为什么要用循环来做而不是map,这在它的源码注释做了说明:
意思是说使用vector比使用map更加节省空间,并且一个dom节点往往不太可能绑了太多的事件类型。这就启示我们写代码要根据实际情况灵活处理。
同时还有一个比较有趣的事情,就是webkit用了一个EventTargetDataMap存放所有节点绑定的事件,它是一个static静态成员变量,被所有Node的实例所共享,由于不同的实例的内存地址不一样,所以它的key不一样,就可以通过内存地址找到它绑的所有事件,即上面说的vector结构。为什么它要用一个类似于全局的变量?按照正常思维,每个Node结点绑的事件是独立的,那应该把绑的事件作为每个Node实例独立的数据,搞一个全局的还得用一个map作一个哈希映射。
一个可能的原因是EventTarget是作为所有DOM结点的事件目标的类,除了Node之外,还有FileReader、AudioNode等也会继承于EventTarget,它们有另外一个EventTargetData。把所有的事件都放一起了,应该会方便统一处理。
这个时候你可能会冒出另外一个问题,这个EventTargetDataMap是什么释放绑定的事件的,我把一个DOM结点删了,它会自动去释放绑定的的事件吗?换句话说,删除掉一个结点前需不需要先off掉它的事件?
2. DOM结点删除与事件解绑
从源码可以看到,Node的析构函数并没有去释放当前Node绑定的事件,所以它是不是不会自动释放事件?为验证,我们在添加绑定一个事件后、删掉结点后分别打印这个map里面的数据,为此给Node添加一个打印的函数:
在上面的第5行,循环打印出所有Node结点的标签名。
同时试验的html如下:
打印的结果如下:
[21755:775:0204/181452.402843:INFO:Node.cpp(1910)] print event map:
[21755:775:0204/181452.403048:INFO:Node.cpp(1912)] “P”[21755:775:0204/181452.404114:INFO:Node.cpp(1910)] print event map:
[21755:775:0204/181452.404287:INFO:Node.cpp(1912)] “P”
[21755:775:0204/181452.404466:INFO:Node.cpp(1912)] “#document”
可以看到remove了p结点之后,它的事件依然存在。
我们看一下blink在remove里面做了什么:
remove是后来W3C新加的api,所以在remove里面调的是老的removeChild,removeChild的关键代码如下: