商城“智能”导航栏实践

需求与目标

在电商的大屏主页上,通常都会有一个显眼的品类导航栏,做为整个商城的重要分流入口,客户体验就必需要作到天然、极致。细心的用户可能会发现,在jd.com或者tmall.com等大型网站中,当鼠标在一级导航栏中垂直移动时,二级菜单能够无延迟的响应展现。神奇的是,当用户将鼠标悬浮在某一级菜单,想去点击对应的二级菜单区域时,即便这时鼠标掠过其余一级菜单,也并无切换到其余二级菜单,彷佛这样的菜单栏很懂你,能够准确预测到你的行为,高大上的叫法是基于用户行为预测的切换技术,我称之为“智能”导航栏,效果以下。javascript

图片描述

在动手实践以前,咱们再来明确一下目标效果:css

  1. 鼠标正常切换一级菜单时,二级菜单无延迟响应;
  2. 鼠标快速移动到二级子菜单时,要求一级菜单无冗余切换;

知识准备

先来把须要用到的知识点划出来。若是完成这样一个小的需求,还能把辐射出的知识点都搞清楚,作到查漏补缺,再把相同的技术衍生到其余的场景,触类旁通,那么这样的实践才是充分的、有价值的。html

  1. 事件代理与事件委托
  2. mouseenter和mouseover的区别;
  3. debounce(防抖)和throttle(节流)
  4. 用向量叉乘判断点在三角形内;(本实践中选择算法4,用叉乘符号相同判断)
  5. 如何高效判断两个数字符号异同;
  6. h5语义化标签--dl dt dd标签元素的语法结构与使用;

对于以上我梳理出来的的知识点,其中第二、第五、第6点比较简单,几句话就能够说清楚,其他三点拿出一条就能够端端正正的写出一篇文章,因此我已把我私藏的优质连接附上,若是你对于某些点比较模糊,请点击跳转学习。java

实践讲解

我会采用渐进加强的方式来进行讲解,完整的示例代码请进codepen算法

基础实现

首先对于文档结构,遵循语义化的原则,左侧的一级菜单用ul li组合.编程

<ul>
    <li data-id="a">
        <span> 一级导航1 </span>
    </li>
    <li data-id="b">
        <span> 一级导航2 </span>
    </li>
    ···
</ul>

右侧的子菜单,用dl dt dd标签来表达,由于他们最经常使用在一个标题下有若干对应列表项的菜单场景。如需进一步了解请点击segmentfault

<div id="sub" class="none">
    <div id="a" class="sub_content none">
        <dl>
            <dt>
                <a href="#"> 二级菜单1 </a>
            </dt>
            <dd>
                <a href="#"> 三级菜单 </a>
                <a href="#"> 三级菜单 </a>
                <a href="#"> 三级菜单 </a>
                <a href="#"> 三级菜单 </a>
                <a href="#"> 三级菜单 </a>
            </dd>
        </dl>
        <dl>
            <dt>
                <a href="#"> 二级菜单1 </a>
            </dt>
            <dd>
                <a href="#"> 三级菜单 </a>
                <a href="#"> 三级菜单 </a>
                <a href="#"> 三级菜单 </a>
                <a href="#"> 三级菜单 </a>
                <a href="#"> 三级菜单 </a>
            </dd>
        </dl>
        ···
    </div>
    <div id="b" class="sub_content none">
        <dl>
            <dt>
                <a href="#"> 二级菜单2 </a>
            </dt>
            <dd>
                <a href="#"> 三级菜单 </a>
                <a href="#"> 三级菜单 </a>
                <a href="#"> 三级菜单 </a>
                <a href="#"> 三级菜单 </a>
                <a href="#"> 三级菜单 </a>
            </dd>
        </dl>
        ···
    </div>
    ···
</div>

接下来,添加js交互。经过鼠标在左侧不一样li的悬浮,来激活显示右侧不一样的.sub_content块,其中经过一级菜单的data-id属性与其id值做为钩子来进行联动。浏览器

这里咱们遇到选择绑定mouseenter仍是mouseover事件,其两者的区别可归纳为:性能优化

  1. 使用mouseover/mouseout时,在鼠标指针通过绑定元素或者通过任何其子元素时,都会触发 mouseover 事件。若是鼠标移动到其子元素上,而没有离开绑定元素,也会触发绑定元素的mouseout事件;
  2. 使用mouseenter/mouseleave时,只有在鼠标指针通过绑定元素时(不包括鼠标指针通过任何子元素),才会触发

mouseenter 事件。若是鼠标没有离开绑定元素,在其子元素上任意移动,也不会触发mouseleave事件;函数

为了助于理解,我作了一个示例,请参考mouseenter/mouseover

经过比较,显然咱们只须要给各li绑定mouseenter/mouseout事件便可。

var sub = $("#sub"); // 子级菜单包裹层
var activeRow, //  已激活的一级菜单
    activeMenu; //  已激活的子级菜单
    
$("#wrap").on("mouseenter", function() {
    // 显示子菜单
    sub.removeClass("none");
})
.on("mouseleave", function() {
    // 隐藏子菜单
    sub.addClass("none");
    // 重置两个已激活变量
    if (activeRow) {
        activeRow.removeClass("active");
        activeRow = null;
    }
    if (activeMenu) {
        activeMenu.addClass("none");
        activeMenu = null;
    }
})
.on("mouseenter", "li", function(e) {
    if (!activeRow) {
        activeRow = $(e.target).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");
});

以上便实现了基本效果,须要注意的是,在知识准备一节中所提到的事件代理的运用,是优化DOM性能的一种很好的实践,同时写法又不失优雅。

然而这个版本在体验上是有问题的,用户为了选择子菜单,必需要谨慎的让鼠标在当前所选一级菜单的范围内,以折线路径移动到子菜单,才能够进一步选择,以下图。

图片描述

很显然,用户但愿在选择某一级菜单下的子菜单时,想要以斜向最短路径移动鼠标,而其余掠过的一级菜单也并不会激活。下面咱们来对此作出改进。

解决斜向移动问题

当鼠标移动时,频繁的触发每个一级菜单所绑定的mouseenter事件是问题的关键。所以咱们很天然的想到延时触发,又为避免频繁触发,引入防抖/节流。每次触发一级菜单时,并不让他当即执行展现子菜单的逻辑,而是延后300ms,直到最后一次触发后300ms,判断鼠标的位置是否在子菜单区域内,若是在,即可直接return不作任何切换菜单操做,以下。

.on("mouseenter", "li", function(e) {
    if (!activeRow) {
        active(e.target);// 一个激活对应子菜单的函数
        return;
    }
    if (timer) {
        clearTimeout(timer);
    }

    timer = setTimeout(function() {
        if (mouseInSub) {
            return;
        }
        activeRow.removeClass("active");
        activeMenu.addClass("none");
        active(e.target);

        timer = null;
    }, 300);
});

由此,由于每一次切换一级菜单,都会有一个延迟300ms触发的效果,因此当用户在一级菜单区域中上下移动时,或者真的想去快速切换菜单时,这样粗糙的延时处理在解决了斜向移动的问题后,又引入了新的问题,以下图。

解决斜方移动问题加入延时显示过慢.gif-1382.7kB

那如何作到当用户真的想要快速切换一级菜单时,子级菜单快速响应,而只有当用户想去选择子级菜单时,才会去运用延时触发,进而能够斜向移动。至此,若是你的知识领域只局限于编程或者计算机科学,那么要解决这个问题着实困难。这里咱们须要些跨学科的启发式思惟,根据用户行为抽象出一个数学模型,进而实现对于用户切换菜单的预测

进一步改善

事实上,咱们能够根据用户鼠标的移动轨迹抽象出这样一个三角形(以下图),构成它的三个点分别是,子级菜单容器的左上顶点(top),及其左下顶点(bottom),另一个是用户鼠标刚刚移动通过的点(pre)。处在三角形内的cur点表明用户鼠标当前的位置。其中pre和cur之间的距离取决于鼠标移动每次触发mousemove事件的粒度,一般会很短很短,这里图例为了方便观察,作了合理放大。

三角形示例.png-108.2kB

这样的一个三角形有何意义呢?在一般的用户行为中,咱们是否能够认为当鼠标在三角形内时,即可以断定用户有选择子级菜单的倾向,当鼠标在三角形外时,此时用户更倾向于快速切换一级菜单。这样在用户不断的移动鼠标时,也同时会不断的造成多个这样的三角形,此时,解决问题的突破口就转化成,不断监听鼠标位置,并判断当前点是否在刚刚通过的点和子级菜单左侧上下两顶点所造成的三角形中

不断监听鼠标位置,咱们能够经过mousemove轻松解决,只须要注意绑定和解绑的时机,让其只在菜单范围内触发,由于持续的监听与触发对于浏览器来说开销不小。而判断一个点是否在一个三角形内,这个问题须要用到知识准备一节中的第四点,咱们选择用向量叉乘符号相同来判断一个点在一个三角形中。至于数学上的证实,不在本文讨论范围内,此处咱们只须要知道该结论是严密的便可。

接下来咱们用代码来模拟实现向量及其叉乘:

// 向量是终点坐标减去起点坐标
function vector(a, b) {
    return {
        x: b.x - a.x,
        y: b.y - a.y
    }
}

// 向量的叉乘
function vectorPro(v1, v2) {
    return v1.x * v2.y - v1.y * v2.x;
}

而后咱们利用上边的两个辅助函数来判断一个点是否在某个三角形内,函数的入参是四个已知的点,最终返回的结果是,所造成的三个向量叉乘后是否两两符号相同,相同即点在三角形内,反之亦反。

// 判断点是否在三角形内
function isPointInTranjgle(p, a, b, c) {
    var pa = vector(p, a);
    var pb = vector(p, b);
    var pc = vector(p, c);

    var t1 = vectorPro(pa, pb);
    var t2 = vectorPro(pb, pc);
    var t3 = vectorPro(pc, pa);

    return sameSign(t1, t2) && sameSign(t2, t3);
}

// 用位运算高效判断符号相同
function sameSign(a, b) {
    return (a ^ b) >= 0;
}

这里须要留意sameSign这个用于判断两个值的符号是否相同的辅助函数,判断符号相同的方法有不少,但此处巧妙的利用了计算机二进制的最高位--符号位。将两个值按位异或,符号位不一样取1,相同取0,因此若是最终符号位为1,即结果值总体小于0,则表明两值符号不一样,反之亦反。位运算的执行效率是要比咱们直接操做非二进制数的执行效率高,因此应用于此处大量频繁地判断符号异同的场景,对于性能优化是颇有帮助的。

最终,咱们利用上边准备好的辅助函数,经过跟踪鼠标的位置信息,判断当前是否须要启用延时器,选择性的实施上一节的优化方案,这样便实现了最终需求。(完整示例代码codepen

// 是否须要延迟
function needDelay(ele, curMouse, prevMouse) {
    if (!curMouse || !prevMouse) {
        return;
    }
    var offset = ele.offset();// offset() 方法返回或设置匹配元素相对于文档的偏移(位置)

    // 左上点
    var topleft = {
        x: offset.left,
        y: offset.top
    };
    // 左下点
    var leftbottom = {
        x: offset.left,
        y: offset.top + ele.height()
    };

    return isPointInTranjgle(curMouse, prevMouse, topleft, leftbottom);
}

启发

经过本例实践,给我最深入的体会即是,高数为提升生产力所带来的价值,哈哈···

恕敝人浅薄,第一次看到这个实例时的那种激动如今依然犹存,再加以前些天翻看了几页深度学习领域的一本经典教材,有大半的篇幅讲所用到的数学知识,不由感叹数学原来是这么玩儿的,惋惜了···

以碾压式的高度和视野去看待问题,可让无解变有解,惟一解变多解,这才是我心目中的高手。

若是这篇文章可让你在coding自己、或者向量(数学)对于其余相似场景(点线面)的应用有所启发,甚至有对于教育引导方面的外延思考,我以为我写这篇文章的目的便达到了。

相关文章
相关标签/搜索