JS开发京东商城无延迟菜单效果
今天带大家来实现京东商城无延迟菜单的效果,希望通过本例子能够让大家学习到菜单的结构和样式处理,菜单常见交互如何开发,同时我还会带大家了解普通二级菜单的常见问题,并通过逐步优化去解决该问题。
如上图是我在京东商城截图的菜单,仅供参考,大家可以去京东官网查看,简单分析一下菜单结构以及我们要实现的交互功能。左边部分是一级菜单,右边部分是二级菜单,当我在一级菜单上移动时,能够很迅速的进行切换,即便是我想点击二级菜单的项目也不会影响其他一级菜单的项目,可以很顺利地对目标进行移动和点击。
本章内容教程是面向的用户是初级前端开发、UI 开发,预备知识是我们前段常用的 HTML、CSS、jQuery 基础即可,所以不难,本案例的实现对于初级的小伙伴具有很好的参考价值。
分类导航菜单的基本布局结构
这里在实现布局的时候需要注意两个问题:
一、当样式切换需要用到 js 来控制的时候,一般用类。如果不需要的话,可以直接用 span:hover 来实现。
如果要用精细的控制,一般用 js 实现。如动画,可以用 css3 实现,如果要实现对帧的控制,就要用 js。
二、给页面添加图标或其他东西的时候,为了减少几个字符,就用元素,可以省去一些字节。
在 HTML 里面不能直接写箭头,因为这样回合元素的闭合以及开始的标签箭头存在二意性,所以必须要用实体。
HTML 代码:
<!--外层容器 用来承载一级菜单和二级菜单 dome 节点--> <div id="test" class="wrap"> <!--一级菜单部分--> <ul> <li data-id="a"> <span>家用电器</span> </li> <li data-id="b"> <span>手机/运营商/数码</span> </li> </ul> <!--二级菜单--> <div id="sub" class="none"> <div id="a" class="sub_content none"> <dl> <dt> <a href="#">电视<i>></i></a> </dt> <dd> <a href="#">合资品牌</a> <a href="#">国产品牌</a> <a href="#">互联网品牌</a> </dd> </dl> <dl> </div> </div> </div>
CSS 代码:
.wrap{ position: relative; width: 200px; left: 50px; top: 50px; } ul{ padding: 15px 0; margin: 0; list-style: none; background: #6c6669; color: #FFF; border-right-width: 0; } li{ display: block; height: 30px; line-height: 30px; padding-left: 12px; cursor: pointer; font-size: 14px; position: relative; } li.active{ background: #999395; } li span:hover{ color: #c81623; } .none{ display: none; } #sub{ width: 600px; position: absolute; border: 1px solid #F7F7F7; background: #F7F7F7; box-shadow: 2px 0 5px rgba(0,0,0,.3); left: 200px; top: 0; box-sizing:border-box; margin: 0; padding: 10px; } .sub_content a{ font-size: 12px; color: #666666; text-decoration: none; } .sub_content dd a{ border-left: 1px solid #e0e0e0; padding: 0 10px; margin: 4px 0; } .sub_content dl{ overflow: hidden; } .sub_content dt{ float: left; width: 78px; font-weight: bold; clear: left; position: relative; } .sub_content dd{ float: left; margin-left: 5px; border-top: 1px solid #EEEEEE; margin-bottom: 5px; } .sub_content dt i { width: 4px; height: 14px; font: 400 9px/14px consolas; }
实现菜单的基本交互
在开发普通的二级菜单前时需要了解我们用到哪些知识点:
一、事件代理方式进行绑定
二、mouseenter 和 mouseover 的区别:
- 使用 mouseenter / mouseover 时,如果鼠标移动到子元素上,即便没有离开父元素,也会触发父元素的 mouseout 事件;
- 使用 mouseenter / mouseover 时,如果鼠标没有离开父元素,在其子元素上任意移动,也不会触发 mouseleave 事件。
具体实现方法:
首先声明三个变量,一是指向子菜单,而是指向当前激活一级菜单中的行,因为后面会对它的样式修改,三是指向二级菜单。
//声明变量指向子菜单 var sub = $("#sub"); //声明指向当前激活一级菜单中的行 var activeRow; //二级菜单 var activeMenu;
当鼠标移动到一级菜单上时把二级菜单显示出来
$('#test').on('mouseenter', function(e){ //当鼠标移动到一级菜单上时把二级菜单显示出来 sub.removeClass('none'); })
当鼠标离开的时候隐藏,这里需要注意鼠标离开一级菜单容器,如果有存在的激活的行,我们要去掉,并把变量置空,同样对于对应的二级菜单是同样的操作。
$('#test').on('mouseleave',function(e){ //鼠标离开的时候隐藏 sub.addClass('none'); //当鼠标离开一级菜单容器,如果有存在的激活的行,我们要去掉 if(activeRow){ activeRow.removeClass('active'); //并把变量置空 activeRow = null; } //同样对于对应的二级菜单是同样的操作 if(activeMenu){ activeMenu.addClass('none'); activeMenu = null; } })
接下来要对一级菜单中的每一项绑定事件,这里我们并不是直接选中所有的列表项,然后循环对每一个绑定,而是采用事件代理方式绑定。好处,首先是假如我们有场景动态的增加和删除这个列表项,如果采用单个绑定的方式可能会造成性能上的问题以及你还要添加代码等等。但是如果把它绑定在父元素上,这样无论是再增加和删除任意多的节点就不需要添加任何代码,你不需要为后续再增加的那些节点再绑定事件,这主要是利用了事件冒泡的特性。推荐大家阅读我之前的一篇文章《event.stopPropagation() event.preventDefault() return false 的区别以及阻止事件冒泡的兼容方法》
然后判断当前移过去的时候没有激活的列表项,这个时候直接把激活的列表项指向事件元素,当从一个已经激活的一级菜单移动到另一个列表的时候,则需要我们清除上次的状态。
$('#test').on('mouseenter','li',function (e){ //当前移过去的时候并没有激活的列表项,所以这个时候直接把激活的列表项指向事件元素 if(!activeRow) { activeRow = $(e.targent).addClass('active'); //选中和它对应的二级菜单 activeMenu = $('#' + activeRow.data('id')) activeMenu.removeClass('none'); return; } //当从一个已经激活的一级菜单移动到另一个列表的时候,清除上次状态 activeRow.removeClass('active'); activeMenu.addClass('none'); activeRow = $(e.target); activeRow.addClass('active'); activeMenu = $('#' + activeRow.data('id')); activeMenu.removeClass('none'); })
此时我们来看一下效果是什么样子的:
此时这个效果咋一看好像没什么问题,但是如果用户希望点击这个二级菜单中稍微偏上或者偏下链接的时候呢,鼠标首先平移到二级菜单中来,从用户的角度考虑,并不符合正常的思路,因为两点之间直线最短,而不是折线方式移动,如图,这种问题在很多网站上常见,他们并没有注意这个问题,接下里我们看看怎样通过延迟来解决这个问题。
利用延迟和去抖技术进行优化
通过上面的问题我们通过加入延迟来优化,这里从两个方面考虑:
- 切换子菜单的时候,用 setTimeout 设置延迟
- debounce 去抖技术 (这个是 js 里常用的一个技术) :意思是在事件频繁触发时,只执行一次处理
这里我们还需要声明一个变量 timer,怎么用呢,我们在 li 的事件中设定一个 300 毫秒的延迟。
$('#test')on('mouseenter','li',function (e){ if(!activeRow) { activeRow = $(e.targent).addClass('active'); //选中和它对应的二级菜单 activeMenu = $('#' + activeRow.data('id')) activeMenu.removeClass('none'); return; } timer = setTimeout(function () { //当从一个已经激活的一级菜单移动到另一个列表的时候,清除上次状态 activeRow.removeClass('active'); activeMenu.addClass('none'); activeRow = $(e.target); activeRow.addClass('active'); activeMenu = $('#' + activeRow.data('id')); activeMenu.removeClass('none'); },300); })
上面代码就是在事件触发的时候,我们设置一个缓冲器,假如这个时间计时器的回调执行的时候呢,如果当前鼠标在这个子菜单里面我们就不进行切换操作,所以,我们还需要声明一个变量 mouseInSub,用来标识当前鼠标是否在子菜单里面。然后我们给这个子菜单绑定事件:
var mouseInSub = false; sub.on('mouseenter',function(e){ mouseInSub = true; }).on('mouseleave',function (e){ mouseInSub = false; })
然后我们在刚设定好的定时器里判断一下
timer = setTimeout(function () { if(mouseInSub){ return; } //当从一个已经激活的一级菜单移动到另一个列表的时候,清除上次状态 activeRow.removeClass('active'); activeMenu.addClass('none'); activeRow = $(e.target); activeRow.addClass('active'); activeMenu = $('#' + activeRow.data('id')); activeMenu.removeClass('none'); },300);
还有一个问题就是当我们连续移动的时候它会有一个连续切换的效果,显然这不是我们想要的效果,这就要引入 debounce 去抖技术了,debounce 目的就是说当我们 mouseenter 事件频繁触发的时候呢,我们只希望它执行最后一次操作,实现方式是这样的,如果这个事件触发的时候,我们这个计时器还没有执行,那我们就把它清掉,那么这个计时器结束回调后,我们把它设为 null,这个就是 debounce 的基本原理。
if(timer){ clearTimeout(timer); } timer = setTimeout(function () { if(mouseInSub){ return; } //当从一个已经激活的一级菜单移动到另一个列表的时候,清除上次状态 activeRow.removeClass('active'); activeMenu.addClass('none'); activeRow = $(e.target); activeRow.addClass('active'); activeMenu = $('#' + activeRow.data('id')); activeMenu.removeClass('none'); timer = null; },300);
当然也有很成熟的 debounce 函数,里面添加了很多其他的功能,但是大概的原理就是这样,感兴趣的小伙伴可以下去查查相关资料。
基于用户行为预测的切换技术
基于用户行为预测的切换技术涉及到的知识点有以下两点:
- 跟踪鼠标的移动
- 用鼠标当前位置,和鼠标上一次位置与子菜单上下边缘形成的三角形区域进行比较
我们记录鼠标的位置,首先要创建一个数组,来跟踪鼠标的位置,接着要给 document 绑定一个事件,mousemove 事件一般是绑定 document 上,这是常识。我们在定义一个 moveHandler 通过 e 也就是事件对象,它的配置 x 和配置 Y 的属性来获取当前鼠标相对于页面坐标,把它放到数组里面保存。由于我们计算的时候只需要当前的位置和上一次位置,所以这个数组只保存有限的位置信息即可,这里我们这顶为 3 个,多余弹出。这里有一个细节需要注意一下,因为我们在鼠标离开菜单的时候需要对绑定在 document 的 mousemove 事件进行解绑,以免影响到页面中其他的组件,所以要把事件监听函数独立出来,方便后续进行解绑操作。
如上图,当用户希望移动到二级菜单的时候呢,他鼠标的轨迹一定是在当前的点 1 上,和二级菜单的上边缘 2 和下边缘 3 构成的三角形内,那么,反过来我们就可以假设,当用户的鼠标在这个三角形内移动的时候,他很可能是想移动到二级菜单上进行操作,而不是移动到一级菜单上,这是一种启发行的思想。
剩下的问题就是判断 如何确定鼠标在三角形内呢?这里就需要用到一些数学知识了,主要包括几个概念,一是向量,二是向量叉乘公式的运用,三是利用叉乘的结果来判断某个点是否在三角形内。
首先是向量的计算,向量的定义就是终点的坐标减去起点的坐标,我们用 x,y 来表示坐标的话,那就是终点的坐标减去起点的坐标,如下代码:
function vector(a,b) { return { //终点的坐标减去起点的坐标 x: b.x - a.x, y: b.y - a.y } }
向量叉乘公式大家可以下去查查资料,是向量 1 的 x 坐标乘以向量 2 的 y 坐标减去向量 2 的 x 坐标乘以向量 1 的 y 坐标,代码如下:
function vectorProduct(v1,v2){ return v1.x * v2.y - v2.x * v1.y }
如何用叉乘判断点在三角形内呢?我们看一下这张图:
当向量 PA、PB、PC 这三个向量,PA 乘以 PB,PB 乘以 PC,以及 PA 乘以 PC,这三个的叉乘结果它们的符号相同的时候,我们就可以证明点 P 在三角形 ABC 内,如果符号不同,就说明点 P 在三角形 ABC 外。这类知识为了实现我们的 demo 提供给大家思路,具体实现过程感兴趣的小伙伴们可以下去查查资料。
回到我们 demo 中,点 P 就是当前我们的鼠标当前的点,点 A 就是鼠标上一次的位置,B 和 C 就是我们二级菜单的上边缘和下边缘。在我们计算之前需要取得这几个点的坐标,P 点的坐标就是我们鼠标点当前的坐标,这个时候就可以从我们刚刚声明保存的数组中拿到,A 点坐标就是鼠标上一次坐标定义为 leftCorner,因为实在一级菜单和二级菜单的左侧,上一次就是数组倒数第二个位置:
var currMousePos = mouseTrank[mouseTrank.length - 1]; var leftCorner = mouseTrank[mouseTrank.length - 2];
那么上下边缘的坐标如何计算呢?我们用 jQuery 的 offset 方法获取上下边缘相对于页面的左上角坐标 ,我们先把叉乘的判断方法写出来:
function isPointInTrangle(p, a, b, c){ var pa = vertor(p, a); var pb = vertor(p, b); var pc = vertor(p, c); var t1 = vectorProduct(pa, pb); var t2 = vectorProduct(pb, pc); var t3 = vectorProduct(pc, pa); }
那么我们如何计算他们的结果是否符号相同呢?这里我们用位运算技巧,定义一个 sameSign 函数,a 异或 b 大于等于 0 时,我们就可以认为它们 a 和 b 符号相同的,原理主要是二进制的正负表示是在最高位,1 表示负,0 表示正,而这个异或运算呢仅当对应的两位有一位为 1 时返回 1,那么返回来就可以证明,如果异或运算的结果是正,也就是首位为 0,那么这两个数一定都为正或者都为负,即符号相同。如果返回的结果为负,那么它们两的首位一个为 0 一个为 1,或者一个 1 一个 0,即符号不相同。
function sameSign(a, b){ return (a ^ b) >= 0; }
然后再 isPointInTrangle 函数中判断:
function isPointInTrangle(p, a, b, c){ var pa = vertor(p, a); var pb = vertor(p, b); var pc = vertor(p, c); var t1 = vectorProduct(pa, pb); var t2 = vectorProduct(pb, pc); var t3 = vectorProduct(pc, pa); return sameSign(t1, t2) && sameSig(t2, t3); }
我们需要的工具都有了,接下来写一个函数判断是否要延迟,接着用 jQuery 的 offset 方法获取上边缘 topLeft 和下边缘 bottomLeft 相对于页面的左上角坐标,我们所需要的信息都有了之后,通过 isPointInTrangle 函数将位置信息传进去,就可以得到点是否在三角形内,也就是决定我们是否需要 delay 延迟。
function needDelay(elem, leftCorner, currMousePos){ var offset = elem.offset(); //上边缘 var topLeft = { x: offset.left, y: offset.top } //下边缘 var bottomLeft = { x: offset.left, y: offset.top + elem.height() } return isPointInTrangle(currMousePos, leftCorner, topLeft, bottomLeft); }
然后加入到现有的代码逻辑中,这里我们把二级菜单传进去,因为哦我们要取上下边缘的坐标,以及鼠标当前的位置和鼠标上一次的位置:
var delay = needDelay(sub, leftCorner, currMousePos)
如上面所过,如果在三角形内,则需要延迟,就把之前的 setTimeout 延迟的逻辑放入,如果不在三角形内,则进行菜单的切换,然后把之前的行和对应的块都分别隐藏,再把当前激活的行和块进行展示。
源码下载:商城无延迟菜单
最终实现效果:
结束语
以上就是今天带给大家的关于 JS 开发京东商城无延迟菜单效果的全部内容,希望对大家有帮助,如果有不明白的地方欢迎留言反馈。
码云笔记 » JS开发京东商城无延迟菜单效果
不错,学习一下