JS冒泡和闭包案例分析

背景:

    今天逛网页发现了百度知道上一个有意思的JS问题,提问者的问题其实蛮简单的,懂点前端开发技术的应该都能实现。提问者的要求:实现子菜单的弹出,菜单共有三级,每级菜单显示时有500毫秒的延迟。然后提问者贴出了他的问题代码。

     对别人贴出来的代码,只要不是特别复杂,我都会看一眼。毕竟程序员交流,源代码是最好的语言,刚开始看他的代码就有点感觉哪里不对。后来仔细分析了下,发现确实是蛮有意思的。如果感觉分析过程比较无聊,可以直接看结论。

    下面是他的代码:

[html]  view plain  copy
 
  1. <html>  
  2.     <head>  
  3.         <style type="text/css">  
  4.             * {  
  5.                 margin: 0;  
  6.                 padding: 0;  
  7.             }  
  8.             #div1 {  
  9.                 position: absolute;  
  10.             }  
  11.             li {  
  12.                 list-style-type: none;  
  13.                 text-align: center;  
  14.                 width: 70px;  
  15.                 height: 30px;  
  16.                 line-height: 30px;  
  17.             }  
  18.             #zg li {  
  19.                 background: blue;  
  20.                 float: left;  
  21.             }  
  22.             #zg li #bj li {  
  23.                 background: green;  
  24.             }  
  25.             #zg li #bj li #xc li {  
  26.                 background: red;  
  27.             }  
  28.             #xc,  
  29.             #sjz {  
  30.                 position: relative;  
  31.                 left: 70px;  
  32.                 top: -30px;  
  33.             }  
  34.             #dc {  
  35.                 position: relative;  
  36.                 left: 70px;  
  37.                 top: -60px;  
  38.             }  
  39.             #bj {  
  40.                 display: none;  
  41.             }  
  42.             #hb {  
  43.                 display: none  
  44.             }  
  45.             #xc {  
  46.                 display: none;  
  47.             }  
  48.             #dc {  
  49.                 display: none;  
  50.             }  
  51.         </style>  
  52.   
  53.         <script type="text/javascript">  
  54.             onload = function() {  
  55.                 var lis = document.getElementsByTagName("li");  
  56.                 var t = 0;  
  57.                 for (var i = 0; i < lis.length; i++) {  
  58.                     lis[i].timer = null;  
  59.                     lis[i].onmouseover = function() {  
  60.                         me = this;  
  61.                         me.timer = setInterval(function() {  
  62.                             if (me.children[0]) {  
  63.                                 me.children[0].style.display = "block"  
  64.                             }  
  65.                         }, 500)  
  66.                     }  
  67.                     lis[i].onmouseout = function() {  
  68.                         clearInterval(me.timer)  
  69.                         this.children[0] ? this.children[0].style.display = "none" : 0;  
  70.                     }  
  71.                 }  
  72.             }  
  73.         </script>  
  74.   
  75.     </head>  
  76.   
  77.     <body>  
  78.         <div id="div1">  
  79.             <ul id="zg">  
  80.                 <li>北京  
  81.                     <ul id="bj">  
  82.                         <li>西城区  
  83.                             <ul id="xc">  
  84.                                 <li>西单</li>  
  85.                                 <li>西单</li>  
  86.                                 <li>西单</li>  
  87.                             </ul>  
  88.                         </li>  
  89.                         <li>东城区  
  90.                             <ul id="dc">  
  91.                                 <li>东单</li>  
  92.                                 <li>东单</li>  
  93.                                 <li>东单</li>  
  94.                             </ul>  
  95.                         </li>  
  96.                         <li>崇文区</li>  
  97.                     </ul>  
  98.                 </li>  
  99.                 <li>河北  
  100.                     <ul id="hb">  
  101.                         <li>石家庄  
  102.                             <ul id="sjz">  
  103.                                 <li>桥东</li>  
  104.                                 <li>桥东</li>  
  105.                                 <li>桥东</li>  
  106.                             </ul>  
  107.                         </li>  
  108.                         <li>保定</li>  
  109.                         <li>邢台</li>  
  110.                     </ul>  
  111.                 </li>  
  112.             </ul>  
  113.         </div>  
  114.     </body>  
  115.   
  116. </html>  

     上面的代码,我们拿出JS部分来研究下:

[javascript]  view plain  copy
 
  1. onload = function() {  
  2.                 var lis = document.getElementsByTagName("li");  
  3.                 var t = 0;  
  4.                 for (var i = 0; i < lis.length; i++) {  
  5.                     lis[i].timer = null;  
  6.                     lis[i].onmouseover = function() {  
  7.                         me = this;  
  8.                         me.timer = setInterval(function() {  
  9.                             if (me.children[0]) {  
  10.                                 me.children[0].style.display = "block"  
  11.                             }  
  12.                         }, 500)  
  13.                     }  
  14.                     lis[i].onmouseout = function() {  
  15.                         clearInterval(me.timer)  
  16.                         this.children[0] ? this.children[0].style.display = "none" : 0;  
  17.                     }  
  18.                 }  
  19.             }  

     首先抛开最基本的错误,比如应该用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的关键字。当然我们这里并不是多线程,没法办法同步这个变量(后面再说解决办法)。

 

    接着分析,后面的两个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编程中,闭包很容易造成内存泄露,谨慎使用,而且闭包变量很容易称为资源竞争对象,尤其是在延迟函数中,很容易发生莫名其妙的问题。

    (3)clearInterval和clearTimeout一定要传入setInterval和setTimeout返回的ID,否则是无法正常取消延迟的。

    (4)onmouseenter和onmouseleave可以作为解决上面问题的一种方案,但是兼容性问题要注意! onmouseenter和onmouseleave不会进行事件冒泡,onmouseover和onmouseout会冒泡到父节点,this指向也会发生变化。

 

PS:

    今天废话了一堆,分析这个案例没什么意思,只是无聊。“不求甚解”固然是求学哲学,但是“追本溯源”才是求解问题的方法。让人给出正确答案远比找出问题原因要容易


你可能感兴趣的:(JavaScript)