背景:
今天逛网页发现了百度知道上一个有意思的JS问题,提问者的问题事实上蛮简单的,懂点前端开发技术的应该都能实现。提问者的要求:实现子菜单的弹出,菜单共同拥有三级。每级菜单显示时有500毫秒的延迟。然后提问者贴出了他的问题代码。
对别人贴出来的代码。仅仅要不是特别复杂,我都会看一眼。
毕竟程序猿交流,源码是最好的语言,刚開始看他的代码就有点感觉哪里不正确。
后来细致分析了下。发现确实是蛮有意思的。
假设感觉分析过程比較无聊。能够直接看结论。
以下是他的代码:
<html> <head> <style type="text/css"> * { margin: 0; padding: 0; } #div1 { position: absolute; } li { list-style-type: none; text-align: center; width: 70px; height: 30px; line-height: 30px; } #zg li { background: blue; float: left; } #zg li #bj li { background: green; } #zg li #bj li #xc li { background: red; } #xc, #sjz { position: relative; left: 70px; top: -30px; } #dc { position: relative; left: 70px; top: -60px; } #bj { display: none; } #hb { display: none } #xc { display: none; } #dc { display: none; } </style> <script type="text/javascript"> onload = function() { var lis = document.getElementsByTagName("li"); var t = 0; for (var i = 0; i < lis.length; i++) { lis[i].timer = null; lis[i].onmouseover = function() { me = this; me.timer = setInterval(function() { if (me.children[0]) { me.children[0].style.display = "block" } }, 500) } lis[i].onmouseout = function() { clearInterval(me.timer) this.children[0] ? this.children[0].style.display = "none" : 0; } } } </script> </head> <body> <div id="div1"> <ul id="zg"> <li>北京 <ul id="bj"> <li>西城区 <ul id="xc"> <li>西单</li> <li>西单</li> <li>西单</li> </ul> </li> <li>东城区 <ul id="dc"> <li>东单</li> <li>东单</li> <li>东单</li> </ul> </li> <li>崇文区</li> </ul> </li> <li>河北 <ul id="hb"> <li>石家庄 <ul id="sjz"> <li>桥东</li> <li>桥东</li> <li>桥东</li> </ul> </li> <li>保定</li> <li>邢台</li> </ul> </li> </ul> </div> </body> </html>
上面的代码,我们拿出JS部分来研究下:
onload = function() { var lis = document.getElementsByTagName("li"); var t = 0; for (var i = 0; i < lis.length; i++) { lis[i].timer = null; lis[i].onmouseover = function() { me = this; me.timer = setInterval(function() { if (me.children[0]) { me.children[0].style.display = "block" } }, 500) } lis[i].onmouseout = function() { clearInterval(me.timer) this.children[0] ? this.children[0].style.display = "none" : 0; } } }
首先抛开最主要的错误,比方应该用setTimeout设置延迟。
问题:
我想非常多人应该都和我有相同的疑问:鼠标移到子菜单后一级菜单应该运行onmouseout方法啊。然后子菜单应该被隐藏才对。
但实际上,假设鼠标移的快的话子菜单有“闪烁”现象可是终于是能够显示的,仅仅是三级菜单显示不出来。
疑问:
难道是onmouseout方法没有运行吗?带着这个疑问。我在onmouseout和onmouseover方法上加上了console.log。并断点调试了下。
结果发现一个有意义的现象。
onmouseout方法确实运行了,也就是子菜单是的style属性变为了none。可是控制台显示。整改方法的运行顺序:父菜单onmouseout -> 子菜单 onmouseover–>
父菜单onmouseover –> 子节点 onmouseout –> 父节点 onmouseout
原因:
出现上面的问题主要原因有三点:
(1)JS的运行是须要耗时的,尤其是设置style的display是要引发render Tree的重构的,所以界面不会里面发现变化。
(2)提问者的onmouseover函数是有益设置了一个500毫秒的延迟来运行显示函数的。
(3)浏览器的事件模型是冒泡运行的。
分析:
当然上面的原因,翻译成人话就是:
当鼠标移到子菜单的时候,浏览器去运行父菜单的onmouseout,这个时候理论上子菜单是须要隐藏的,可是由于第一点原因,子菜单没有隐藏,我们的鼠标移动到了二级菜单上。这个时候浏览器须要触发二级菜单上的onmouseover方法,然后运行子菜单的onmouseover方法。并设置了延迟函数,由于我们并没有阻止事件冒泡,二级菜单的onmouseover冒泡到一级菜单上,运行父菜单的onmouseover。
分析到上面,我们应该注意按理说二级菜单的onmouseover中应该把它子菜单(三级菜单)显示出来了。可是我们要注意onmouseover的逻辑并非正常的逻辑,而是一种延迟。所以二级菜单的show函数(也就是那个匿名函数)运行是发生在一级菜单的onmouseover函数后的。所以me这个闭包变量被改变了。
通过调试,能够发现me这个变量是挂载在windows下的。那么它就是一个全局变量了,子菜单的onmouseover把它设置为子节点本身,可是接着冒泡到父节点。onmouseover的this变为了父节点本身,所以终于show函数被运行两次。两次的me变量都是父菜单相应的节点。
上面这个问题有点类似高级程序设计语言(如Java)中进行多线程编程的资源竞争了,在高级程序语言中,为了同步一个资源(变量、对象)。语言本身会提供类似synchronized的keyword。当然我们这里并非多线程,没法办法同步这个变量(后面再说解决的方法)。
接着分析,后面的两个onmouseout。事实上更好理解了,当程序运行到这里的时候。浏览器最终完毕了render Tree的重构。子菜单就隐藏了,然后浏览器就觉得我们的鼠标移出了子菜单,所以開始出发子菜单的onmouseout事件,而且是冒泡运行的,详细过程不做分析了。
程序运行完两次onmouseout后。肯定不会花费500ms,除非你的机器特别老,所以这个时候,我们设置的两个延迟函数会被触发。然后运行me的子节点的显示,我们上面提到了两次show函数的me实际上是同一个变量。都指向了父菜单相应的节点。所以子节点就被显示了。
到了这里。我们的问题又来了。onmouseout里面调用了clearTimeout,那么两个延迟函数应该被取消啊。答案就是,clearTimeout使用方法不正确。
相信非常多人和楼主我一样。并没有把JS当一门完整的技术学过。所以我们知道setTimeout和setInterval。知道用相应的clear方法取消。可是我们从没研究过它的执行机制(当然非常多时候不是必需,就好比Java刚出来的时候,非常少有人关心JVM层次的东西)。这里我们不深究。仅仅给出结论,从W3C上就能够知道。
setTimeout和setInterval都会返回一个字符串(类似Thread
ID),调用clear方法时。须要传入相应的ID,这样才干停止运行延迟函数。
最后一个问题,既然子菜单经过了隐藏显示过程。那么为什么浏览器触发了onmouseout没有再次触发onmouseover。从而形成死循环,让页面死掉呢?至于这个问题,我也没有找到标准答案,或许浏览器对于mouse事件的触发是依照像素触发的吧,我们没有再移动鼠标,自然不会触发onmouseover了。
结论:
废话了一堆。非常多人应该都不耐烦了,说下结论吧。
(1)JS编程,我们一定要注意事件冒泡,冒泡是个好东西,可是要慎重使用。事件冒泡能让我们提高程序效率,比方table操作,非常多时候我们会让事件冒泡到table上再进行操作。这样能有非常高的运行效率。jQuery的事件处理就非常好的利用了冒泡。
(2)慎重使用闭包,JS编程中,闭包非常easy造成内存泄露。慎重使用,并且闭包变量非常easy称为资源竞争对象。尤其是在延迟函数中。非常easy发生莫名其妙的问题。
(3)clearInterval和clearTimeout一定要传入setInterval和setTimeout返回的ID。否则是无法正常取消延迟的。
(4)onmouseenter和onmouseleave能够作为解决上面问题的一种方案。可是兼容性问题要注意!
onmouseenter和onmouseleave不会进行事件冒泡,onmouseover和onmouseout会冒泡到父节点,this指向也会发生变化。
PS:
今天废话了一堆。分析这个案例没什么意思,仅仅是无聊。
“不求甚解”固然是求学哲学,可是“追本溯源”才是求解问题的方法。让人给出正确答案远比找出问题原因要easy。