涉及到的知识点(函数节流、this指向、事件冒泡、事件代理)

源码在文章底部

后续有优化版本

先把菜单最终效果图给大家搞上来

这里先把html和css代码贴上来
html部分
函数节流实现滑动下拉菜单-小白菜博客
css部分
`

.main{
    width: 260px;
    margin: 0 auto;
    margin-top: 50px;
}
.main ul{
    list-style: none;
}
.main ul li {
    position: relative;
    border: 1px black solid;
    line-height: 40px;
    text-indent: 2em;
    font-size: 16px;
}
.main ul>li:first-child {
    border-top-right-radius: 10px;
    border-top-left-radius: 10px;
}
.main ul>li:last-child{
    border-bottom-left-radius: 10px;
    border-bottom-right-radius: 10px;

}
.main ul li span {
    display: block;
    position: absolute;
    right: 10px;
    top: 5px;
    height: 30px;
    width: 30px;
    background: url(./imgs/箭头.png) no-repeat center center;
    background-size: contain;
}
.sub {
    display: none;
    background-color: #333;
    overflow: hidden;
}
.main ul li .sub li {
    box-sizing: border-box;
    color: blanchedalmond;
    border: 0px black solid;
    border-bottom: 1px black solid;
    border-bottom-left-radius: 0px;
    border-bottom-right-radius: 0px;

}
.sub li:hover {
    background-color: black;
}
.current {
    transform: rotate(90deg);
}

`

li高度问题

其中有一个问题还是要重视一下的。
那就是一级菜单的高度不要用height设置否则二级菜单会覆盖别的菜单
如下

使用line-height撑开就好了

函数节流

函数节流是通过对定时器的操作保证一段时间内只执行一次事件触发的函数的技术,
思路:第一次触发函数时设置一个定时器,如果再次触发该事件时定时器的回调函数还未执行,
就不作任何回应,而当定时器执行完回调函数时将定时器清除,以保证下一次事件的正常触发。

接下来我们写一下jq实现鼠标滑过弹出二级菜单的效果

`

        let toggle = function(){
            //children()选中指定子元素 | siblings()选中同级元素 | slideDown(时间)/slideUp(毫秒)滑动展开/闭合
            $(this).children('span').addClass('current');                  //给当前箭头添加.current使箭头变向
            $(this).siblings().children('span').removeClass('current');    //把其他箭头的.curretn类名去掉
            $sub = $(this).children('.sub');                               //选中当前一级菜单
            $sub.slideDown(500);                                           //将当前一级菜单展开
            $(this).siblings().children('.sub').slideUp(500)               //将其他菜单闭合
        }

`
但是我们把这个函数绑定到滑动事件上会出现这样的情况

我们可以看到即使鼠标不在滑动它还是在不断的触发事件对应的方法。
这是因为鼠标多次滑过这几个菜单导致大量的事件函数产生,而js要将这些函数全部执行一遍虽然很快,
但滑动效果的执行需要时间,导致这种令人头疼的现象出现。
而我们要做的是对事件触发进行节流
`

        //节流
        var throttle = function(func, delay) {            
            var timer = null; //定义一个定时器变量
            return function() { 
                    //保存当前执行上下文(对象环境),原因接下来会说    
                var context = this;   
                    //如果没有设置定时器就设置一个,否则不作反馈            
                if (!timer) {                    
                    timer = setTimeout(function() {                        
                        func.apply(context);    //把func(也就是toggle)绑定到之前保存的执行上下文中
                        //将定时器变量置空,以保证下一次事件的正常触发
                        timer = null;                    
                    }, delay);                
                }            
            }        
        } 
        $('ul>li').hover(throttle(toggle,100)); //建议设置40~60ms

`

this指向问题

众所周知
-普通/匿名函数中的this会指向调用者所在的对象(执行上下文)
-而箭头函数中的this则永久指向一开始定义时所在的对象(执行上下文)
-绑定的this指向会覆盖箭头函数中的this指向

那么我们就会发现在函数中因为要通过定时器的回调函数来执行toggle()函数,所以执行回调函数时其中的this会指向定时器setTimeout所在的window对象
所以我们需要在执行回调函数之前用apply()方法将func绑定到了this指向的对象(也就是当前的li元素)

事件冒泡与代理

在我们解决了事件频繁触发的问题之后,我们又遇到了新的问题,那就是看起来并没有解决。
在这个案例中如果我们不通过父元素对子元素进行事件代理的话,会导致每个一级菜单触发的事件都是独立与另一个一级菜单的,而这样即使我们进行节流也没有意义因为事件源不统一,频繁触发的事件并不来自同一个地方。

所以我们需要通过对父元素(ul)来代理一级菜单的事件达到事件源的统一

$('ul').delegate('li','hover',throttle(toggle,50));

但当我们这样设置之后发现,菜单竟然不展开了。

发生这种情况的原因是hover()为非标准事件不可以进行事件代理。
那我们知道hover是对mouseenter和mouseleave的实现所以我们用其中一个也可以实现类似的效果

$('ul').delegate('li','mouseenter',throttle(toggle,50));
至此我们就成功的实现了滑动下拉菜单并解决其中遇到问题/撒花

新版本来了

优化版:鼠标滑出时关闭所有一级菜单。

  • 一开始以为只需要通过ul对下级菜单的一个代理就能解决问题 可惜我年轻了呀,
  • 这里之所以用到了两个ul代理是因为需要触发两次mouseleave,那为什么需要触发两次呢?
  • 因为当鼠标离开整个菜单时可能是从一级菜单离开也可能是从二级菜单离开
  • 导致mouseleave事件触发时this指向是不同的,分别指向一级菜单与二级菜单。
  • 那么我们就需要两次触发(而且是对不同子元素的代理分别为.sub与.first)分别处理

`

        $('ul').delegate('.sub','mouseleave',throttle(close,50));
        $('ul').delegate('.first','mouseleave',throttle(close,50));

菜单关闭函数:

        let close = function(){
            //对事件的触发元素进行判断分别处理。
            if($(this).hasClass('sub') ){
                console.log("触发sub")
                $(this).slideUp(500);
                $(this).parent().children('span').removeClass('current');
            }
            if($(this).hasClass('first')){
                console.log("触发first")
                $(this).children('.sub').slideUp(500);
                $(this).children('span').removeClass('current');
            }
            console.log(this)
        }

`
链接已更新
源码链接