摘要:細心的用戶可能會發現,在或者等大型網站中,當鼠標在一級導航欄中垂直移動時,二級菜單可以無延遲的響應展示。很顯然,用戶希望在選擇某一級菜單下的子菜單時,想要以斜向最短路徑移動鼠標,而其他掠過的一級菜單也并不會激活。
需求與目標
在電商的大屏主頁上,一般都會有一個顯眼的品類導航欄,作為整個商城的重要分流入口,客戶體驗就必須要做到自然、極致。細心的用戶可能會發現,在jd.com或者tmall.com等大型網站中,當鼠標在一級導航欄中垂直移動時,二級菜單可以無延遲的響應展示。神奇的是,當用戶將鼠標懸浮在某一級菜單,想去點擊對應的二級菜單區域時,即使這時鼠標掠過其他一級菜單,也并沒有切換到其他二級菜單,似乎這樣的菜單欄很懂你,可以準確預測到你的行為,高大上的叫法是基于用戶行為預測的切換技術,我稱之為“智能”導航欄,效果如下。
在動手實踐之前,我們再來明確一下目標效果:
知識準備鼠標正常切換一級菜單時,二級菜單無延遲響應;
鼠標快速移動到二級子菜單時,要求一級菜單無冗余切換;
先來把需要用到的知識點劃出來。如果完成這樣一個小的需求,還能把輻射出的知識點都搞清楚,做到查漏補缺,再把相同的技術衍生到其他的場景,舉一反三,那么這樣的實踐才是充分的、有價值的。
事件代理與事件委托;
mouseenter和mouseover的區別;
debounce(防抖)和throttle(節流);
用向量叉乘判斷點在三角形內;(本實踐中選擇算法4,用叉乘符號相同判斷)
如何高效判斷兩個數字符號異同;
h5語義化標簽--dl dt dd標簽元素的語法結構與使用;
對于以上我梳理出來的的知識點,其中第2、第5、第6點比較簡單,幾句話就可以說清楚,其余三點拿出一條就可以端端正正的寫出一篇文章,所以我已把我私藏的優質鏈接附上,如果你對于某些點比較模糊,請點擊跳轉學習。
實踐講解我會采用漸進增強的方式來進行講解,完整的示例代碼請進codepen。
基礎實現首先對于文檔結構,遵循語義化的原則,左側的一級菜單用ul li組合.
右側的子菜單,用dl dt dd標簽來表達,因為他們最常用在一個標題下有若干對應列表項的菜單場景。如需進一步了解請點擊。
接下來,添加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本身、或者向量(數學)對于其他類似場景(點線面)的應用有所啟發,甚至有對于教育引導方面的外延思考,我覺得我寫這篇文章的目的便達到了。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/90493.html
摘要:主體內容區域小米首頁下小米商城的主題內容區域,也是整體網頁面積最廣的區塊實在不知道定主體內容區塊時也可以根據面積比重來劃分,最大的那塊一定是主題中心,布局的重復性很高。 單就深入了解布局規范都足夠說上一個月的,今天我就不論大范圍,挑選小米網站首頁的部分區塊布局來講解吧! 下面是小米官網的首頁,很多人一看到這樣的網頁就傻眼,不知道咋弄,要么就隨性布局,要么就干看著,其實遇到問題首先一點就...
摘要:主體內容區域小米首頁下小米商城的主題內容區域,也是整體網頁面積最廣的區塊實在不知道定主體內容區塊時也可以根據面積比重來劃分,最大的那塊一定是主題中心,布局的重復性很高。 單就深入了解布局規范都足夠說上一個月的,今天我就不論大范圍,挑選小米網站首頁的部分區塊布局來講解吧! 下面是小米官網的首頁,很多人一看到這樣的網頁就傻眼,不知道咋弄,要么就隨性布局,要么就干看著,其實遇到問題首先一點就...
摘要:一般來講,我們的網頁導航欄是這么個模式來構建在結構上首先我們需要給導航欄的給個類名一般為然后就是一個無序表格由于導航欄的文字一般都是鏈接用來跳轉頁面要在里面包含一個首頁云云商城智慧門店營銷平臺媒體聯盟關于云道在樣式上目前我見過的分為兩種導航一般來講,我們的網頁導航欄是這么個模式來構建在結構上:1.首先我們需要給導航欄的div 給個類名 一般為nav2.然后就是一個無序表格?3.由于導航欄的文...
閱讀 3403·2021-11-24 09:38
閱讀 3189·2021-11-22 09:34
閱讀 2098·2021-09-22 16:03
閱讀 2349·2019-08-29 18:37
閱讀 371·2019-08-29 16:15
閱讀 1761·2019-08-26 13:56
閱讀 853·2019-08-26 12:21
閱讀 2198·2019-08-26 12:15