在電商的大屏主頁上,通常都會有一個顯眼的品類導航欄,做爲整個商城的重要分流入口,客戶體驗就必需要作到天然、極致。細心的用戶可能會發現,在jd.com或者tmall.com等大型網站中,當鼠標在一級導航欄中垂直移動時,二級菜單能夠無延遲的響應展現。神奇的是,當用戶將鼠標懸浮在某一級菜單,想去點擊對應的二級菜單區域時,即便這時鼠標掠過其餘一級菜單,也並無切換到其餘二級菜單,彷佛這樣的菜單欄很懂你,能夠準確預測到你的行爲,高大上的叫法是基於用戶行爲預測的切換技術,我稱之爲「智能」導航欄,效果以下。javascript
在動手實踐以前,咱們再來明確一下目標效果:css
- 鼠標正常切換一級菜單時,二級菜單無延遲響應;
- 鼠標快速移動到二級子菜單時,要求一級菜單無冗餘切換;
先來把須要用到的知識點劃出來。若是完成這樣一個小的需求,還能把輻射出的知識點都搞清楚,作到查漏補缺,再把相同的技術衍生到其餘的場景,觸類旁通,那麼這樣的實踐纔是充分的、有價值的。html
對於以上我梳理出來的的知識點,其中第二、第五、第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
事件,其兩者的區別可歸納爲:性能優化
- 使用mouseover/mouseout時,在鼠標指針通過綁定元素或者通過任何其子元素時,都會觸發 mouseover 事件。若是鼠標移動到其子元素上,而沒有離開綁定元素,也會觸發綁定元素的mouseout事件;
- 使用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觸發的效果,因此當用戶在一級菜單區域中上下移動時,或者真的想去快速切換菜單時,這樣粗糙的延時處理在解決了斜向移動的問題後,又引入了新的問題,以下圖。
那如何作到當用戶真的想要快速切換一級菜單時,子級菜單快速響應,而只有當用戶想去選擇子級菜單時,纔會去運用延時觸發,進而能夠斜向移動。至此,若是你的知識領域只侷限於編程或者計算機科學,那麼要解決這個問題着實困難。這裏咱們須要些跨學科的啓發式思惟,根據用戶行爲抽象出一個數學模型,進而實現對於用戶切換菜單的預測。
事實上,咱們能夠根據用戶鼠標的移動軌跡抽象出這樣一個三角形(以下圖),構成它的三個點分別是,子級菜單容器的左上頂點(top),及其左下頂點(bottom),另一個是用戶鼠標剛剛移動通過的點(pre)。處在三角形內的cur點表明用戶鼠標當前的位置。其中pre和cur之間的距離取決於鼠標移動每次觸發mousemove事件的粒度,一般會很短很短,這裏圖例爲了方便觀察,作了合理放大。
這樣的一個三角形有何意義呢?在一般的用戶行爲中,咱們是否能夠認爲當鼠標在三角形內時,即可以斷定用戶有選擇子級菜單的傾向,當鼠標在三角形外時,此時用戶更傾向於快速切換一級菜單。這樣在用戶不斷的移動鼠標時,也同時會不斷的造成多個這樣的三角形,此時,解決問題的突破口就轉化成,不斷監聽鼠標位置,並判斷當前點是否在剛剛通過的點和子級菜單左側上下兩頂點所造成的三角形中。
不斷監聽鼠標位置,咱們能夠經過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自己、或者向量(數學)對於其餘相似場景(點線面)的應用有所啓發,甚至有對於教育引導方面的外延思考,我以爲我寫這篇文章的目的便達到了。