摘要:細(xì)心的用戶可能會(huì)發(fā)現(xiàn),在或者等大型網(wǎng)站中,當(dāng)鼠標(biāo)在一級(jí)導(dǎo)航欄中垂直移動(dòng)時(shí),二級(jí)菜單可以無(wú)延遲的響應(yīng)展示。很顯然,用戶希望在選擇某一級(jí)菜單下的子菜單時(shí),想要以斜向最短路徑移動(dòng)鼠標(biāo),而其他掠過的一級(jí)菜單也并不會(huì)激活。
需求與目標(biāo)
在電商的大屏主頁(yè)上,一般都會(huì)有一個(gè)顯眼的品類導(dǎo)航欄,作為整個(gè)商城的重要分流入口,客戶體驗(yàn)就必須要做到自然、極致。細(xì)心的用戶可能會(huì)發(fā)現(xiàn),在jd.com或者tmall.com等大型網(wǎng)站中,當(dāng)鼠標(biāo)在一級(jí)導(dǎo)航欄中垂直移動(dòng)時(shí),二級(jí)菜單可以無(wú)延遲的響應(yīng)展示。神奇的是,當(dāng)用戶將鼠標(biāo)懸浮在某一級(jí)菜單,想去點(diǎn)擊對(duì)應(yīng)的二級(jí)菜單區(qū)域時(shí),即使這時(shí)鼠標(biāo)掠過其他一級(jí)菜單,也并沒有切換到其他二級(jí)菜單,似乎這樣的菜單欄很懂你,可以準(zhǔn)確預(yù)測(cè)到你的行為,高大上的叫法是基于用戶行為預(yù)測(cè)的切換技術(shù),我稱之為“智能”導(dǎo)航欄,效果如下。
在動(dòng)手實(shí)踐之前,我們?cè)賮砻鞔_一下目標(biāo)效果:
知識(shí)準(zhǔn)備鼠標(biāo)正常切換一級(jí)菜單時(shí),二級(jí)菜單無(wú)延遲響應(yīng);
鼠標(biāo)快速移動(dòng)到二級(jí)子菜單時(shí),要求一級(jí)菜單無(wú)冗余切換;
先來把需要用到的知識(shí)點(diǎn)劃出來。如果完成這樣一個(gè)小的需求,還能把輻射出的知識(shí)點(diǎn)都搞清楚,做到查漏補(bǔ)缺,再把相同的技術(shù)衍生到其他的場(chǎng)景,舉一反三,那么這樣的實(shí)踐才是充分的、有價(jià)值的。
事件代理與事件委托;
mouseenter和mouseover的區(qū)別;
debounce(防抖)和throttle(節(jié)流);
用向量叉乘判斷點(diǎn)在三角形內(nèi);(本實(shí)踐中選擇算法4,用叉乘符號(hào)相同判斷)
如何高效判斷兩個(gè)數(shù)字符號(hào)異同;
h5語(yǔ)義化標(biāo)簽--dl dt dd標(biāo)簽元素的語(yǔ)法結(jié)構(gòu)與使用;
對(duì)于以上我梳理出來的的知識(shí)點(diǎn),其中第2、第5、第6點(diǎn)比較簡(jiǎn)單,幾句話就可以說清楚,其余三點(diǎn)拿出一條就可以端端正正的寫出一篇文章,所以我已把我私藏的優(yōu)質(zhì)鏈接附上,如果你對(duì)于某些點(diǎn)比較模糊,請(qǐng)點(diǎn)擊跳轉(zhuǎn)學(xué)習(xí)。
實(shí)踐講解我會(huì)采用漸進(jìn)增強(qiáng)的方式來進(jìn)行講解,完整的示例代碼請(qǐng)進(jìn)codepen。
基礎(chǔ)實(shí)現(xiàn)首先對(duì)于文檔結(jié)構(gòu),遵循語(yǔ)義化的原則,左側(cè)的一級(jí)菜單用ul li組合.
右側(cè)的子菜單,用dl dt dd標(biāo)簽來表達(dá),因?yàn)樗麄冏畛S迷谝粋€(gè)標(biāo)題下有若干對(duì)應(yīng)列表項(xiàng)的菜單場(chǎng)景。如需進(jìn)一步了解請(qǐng)點(diǎn)擊。
接下來,添加js交互。通過鼠標(biāo)在左側(cè)不同li的懸浮,來激活顯示右側(cè)不同的.sub_content塊,其中通過一級(jí)菜單的data-id屬性與其id值作為鉤子來進(jìn)行聯(lián)動(dòng)。
這里我們遇到選擇綁定mouseenter還是mouseover事件,其二者的區(qū)別可概括為:
使用mouseover/mouseout時(shí),在鼠標(biāo)指針經(jīng)過綁定元素或者經(jīng)過任何其子元素時(shí),都會(huì)觸發(fā) mouseover 事件。如果鼠標(biāo)移動(dòng)到其子元素上,而沒有離開綁定元素,也會(huì)觸發(fā)綁定元素的mouseout事件;
使用mouseenter/mouseleave時(shí),只有在鼠標(biāo)指針經(jīng)過綁定元素時(shí)(不包括鼠標(biāo)指針經(jīng)過任何子元素),才會(huì)觸發(fā)
mouseenter 事件。如果鼠標(biāo)沒有離開綁定元素,在其子元素上任意移動(dòng),也不會(huì)觸發(fā)mouseleave事件;
為了助于理解,我做了一個(gè)示例,請(qǐng)參考mouseenter/mouseover。
通過比較,顯然我們只需要給各li綁定mouseenter/mouseout事件即可。
var sub = $("#sub"); // 子級(jí)菜單包裹層 var activeRow, // 已激活的一級(jí)菜單 activeMenu; // 已激活的子級(jí)菜單 $("#wrap").on("mouseenter", function() { // 顯示子菜單 sub.removeClass("none"); }) .on("mouseleave", function() { // 隱藏子菜單 sub.addClass("none"); // 重置兩個(gè)已激活變量 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"); });
以上便實(shí)現(xiàn)了基本效果,需要注意的是,在知識(shí)準(zhǔn)備一節(jié)中所提到的事件代理的運(yùn)用,是優(yōu)化DOM性能的一種很好的實(shí)踐,同時(shí)寫法又不失優(yōu)雅。
然而這個(gè)版本在體驗(yàn)上是有問題的,用戶為了選擇子菜單,必須要謹(jǐn)慎的讓鼠標(biāo)在當(dāng)前所選一級(jí)菜單的范圍內(nèi),以折線路徑移動(dòng)到子菜單,才可以進(jìn)一步選擇,如下圖。
很顯然,用戶希望在選擇某一級(jí)菜單下的子菜單時(shí),想要以斜向最短路徑移動(dòng)鼠標(biāo),而其他掠過的一級(jí)菜單也并不會(huì)激活。下面我們來對(duì)此做出改進(jìn)。
解決斜向移動(dòng)問題當(dāng)鼠標(biāo)移動(dòng)時(shí),頻繁的觸發(fā)每一個(gè)一級(jí)菜單所綁定的mouseenter事件是問題的關(guān)鍵。因此我們很自然的想到延時(shí)觸發(fā),又為避免頻繁觸發(fā),引入防抖/節(jié)流。每次觸發(fā)一級(jí)菜單時(shí),并不讓他立即執(zhí)行展示子菜單的邏輯,而是延后300ms,直到最后一次觸發(fā)后300ms,判斷鼠標(biāo)的位置是否在子菜單區(qū)域內(nèi),如果在,便可直接return不做任何切換菜單操作,如下。
.on("mouseenter", "li", function(e) { if (!activeRow) { active(e.target);// 一個(gè)激活對(duì)應(yīng)子菜單的函數(shù) return; } if (timer) { clearTimeout(timer); } timer = setTimeout(function() { if (mouseInSub) { return; } activeRow.removeClass("active"); activeMenu.addClass("none"); active(e.target); timer = null; }, 300); });
由此,因?yàn)槊恳淮吻袚Q一級(jí)菜單,都會(huì)有一個(gè)延遲300ms觸發(fā)的效果,所以當(dāng)用戶在一級(jí)菜單區(qū)域中上下移動(dòng)時(shí),或者真的想去快速切換菜單時(shí),這樣粗糙的延時(shí)處理在解決了斜向移動(dòng)的問題后,又引入了新的問題,如下圖。
那如何做到當(dāng)用戶真的想要快速切換一級(jí)菜單時(shí),子級(jí)菜單快速響應(yīng),而只有當(dāng)用戶想去選擇子級(jí)菜單時(shí),才會(huì)去運(yùn)用延時(shí)觸發(fā),進(jìn)而可以斜向移動(dòng)。至此,如果你的知識(shí)領(lǐng)域只局限于編程或者計(jì)算機(jī)科學(xué),那么要解決這個(gè)問題著實(shí)困難。這里我們需要些跨學(xué)科的啟發(fā)式思維,根據(jù)用戶行為抽象出一個(gè)數(shù)學(xué)模型,進(jìn)而實(shí)現(xiàn)對(duì)于用戶切換菜單的預(yù)測(cè)。
進(jìn)一步改善事實(shí)上,我們可以根據(jù)用戶鼠標(biāo)的移動(dòng)軌跡抽象出這樣一個(gè)三角形(如下圖),構(gòu)成它的三個(gè)點(diǎn)分別是,子級(jí)菜單容器的左上頂點(diǎn)(top),及其左下頂點(diǎn)(bottom),另外一個(gè)是用戶鼠標(biāo)剛剛移動(dòng)經(jīng)過的點(diǎn)(pre)。處在三角形內(nèi)的cur點(diǎn)代表用戶鼠標(biāo)當(dāng)前的位置。其中pre和cur之間的距離取決于鼠標(biāo)移動(dòng)每次觸發(fā)mousemove事件的粒度,通常會(huì)很短很短,這里圖例為了方便觀察,做了合理放大。
這樣的一個(gè)三角形有何意義呢?在通常的用戶行為中,我們是否可以認(rèn)為當(dāng)鼠標(biāo)在三角形內(nèi)時(shí),便可以判定用戶有選擇子級(jí)菜單的傾向,當(dāng)鼠標(biāo)在三角形外時(shí),此時(shí)用戶更傾向于快速切換一級(jí)菜單。這樣在用戶不斷的移動(dòng)鼠標(biāo)時(shí),也同時(shí)會(huì)不斷的形成多個(gè)這樣的三角形,此時(shí),解決問題的突破口就轉(zhuǎn)化成,不斷監(jiān)聽鼠標(biāo)位置,并判斷當(dāng)前點(diǎn)是否在剛剛經(jīng)過的點(diǎn)和子級(jí)菜單左側(cè)上下兩頂點(diǎn)所形成的三角形中。
不斷監(jiān)聽鼠標(biāo)位置,我們可以通過mousemove輕松解決,只需要注意綁定和解綁的時(shí)機(jī),讓其只在菜單范圍內(nèi)觸發(fā),因?yàn)槌掷m(xù)的監(jiān)聽與觸發(fā)對(duì)于瀏覽器來講開銷不小。而判斷一個(gè)點(diǎn)是否在一個(gè)三角形內(nèi),這個(gè)問題需要用到知識(shí)準(zhǔn)備一節(jié)中的第四點(diǎn),我們選擇用向量叉乘符號(hào)相同來判斷一個(gè)點(diǎn)在一個(gè)三角形中。至于數(shù)學(xué)上的證明,不在本文討論范圍內(nèi),此處我們只需要知道該結(jié)論是嚴(yán)密的即可。
接下來我們用代碼來模擬實(shí)現(xiàn)向量及其叉乘:
// 向量是終點(diǎn)坐標(biāo)減去起點(diǎn)坐標(biāo) 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; }
然后我們利用上邊的兩個(gè)輔助函數(shù)來判斷一個(gè)點(diǎn)是否在某個(gè)三角形內(nèi),函數(shù)的入?yún)⑹撬膫€(gè)已知的點(diǎn),最終返回的結(jié)果是,所形成的三個(gè)向量叉乘后是否兩兩符號(hào)相同,相同即點(diǎn)在三角形內(nèi),反之亦反。
// 判斷點(diǎn)是否在三角形內(nèi) 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); } // 用位運(yùn)算高效判斷符號(hào)相同 function sameSign(a, b) { return (a ^ b) >= 0; }
這里需要留意sameSign這個(gè)用于判斷兩個(gè)值的符號(hào)是否相同的輔助函數(shù),判斷符號(hào)相同的方法有很多,但此處巧妙的利用了計(jì)算機(jī)二進(jìn)制的最高位--符號(hào)位。將兩個(gè)值按位異或,符號(hào)位不同取1,相同取0,所以如果最終符號(hào)位為1,即結(jié)果值整體小于0,則代表兩值符號(hào)不同,反之亦反。位運(yùn)算的執(zhí)行效率是要比我們直接操作非二進(jìn)制數(shù)的執(zhí)行效率高,所以應(yīng)用于此處大量頻繁地判斷符號(hào)異同的場(chǎng)景,對(duì)于性能優(yōu)化是很有幫助的。
最終,我們利用上邊準(zhǔn)備好的輔助函數(shù),通過跟蹤鼠標(biāo)的位置信息,判斷當(dāng)前是否需要啟用延時(shí)器,選擇性的實(shí)施上一節(jié)的優(yōu)化方案,這樣便實(shí)現(xiàn)了最終需求。(完整示例代碼codepen)
// 是否需要延遲 function needDelay(ele, curMouse, prevMouse) { if (!curMouse || !prevMouse) { return; } var offset = ele.offset();// offset() 方法返回或設(shè)置匹配元素相對(duì)于文檔的偏移(位置) // 左上點(diǎn) var topleft = { x: offset.left, y: offset.top }; // 左下點(diǎn) var leftbottom = { x: offset.left, y: offset.top + ele.height() }; return isPointInTranjgle(curMouse, prevMouse, topleft, leftbottom); }啟發(fā)
通過本例實(shí)踐,給我最深刻的體會(huì)便是,高數(shù)為提高生產(chǎn)力所帶來的價(jià)值,哈哈···
恕敝人淺薄,第一次看到這個(gè)實(shí)例時(shí)的那種激動(dòng)現(xiàn)在依然猶存,再加之前些天翻看了幾頁(yè)深度學(xué)習(xí)領(lǐng)域的一本經(jīng)典教材,有大半的篇幅講所用到的數(shù)學(xué)知識(shí),不禁感嘆數(shù)學(xué)原來是這么玩兒的,可惜了···
以碾壓式的高度和視野去看待問題,可以讓無(wú)解變有解,唯一解變多解,這才是我心目中的高手。
如果這篇文章可以讓你在coding本身、或者向量(數(shù)學(xué))對(duì)于其他類似場(chǎng)景(點(diǎn)線面)的應(yīng)用有所啟發(fā),甚至有對(duì)于教育引導(dǎo)方面的外延思考,我覺得我寫這篇文章的目的便達(dá)到了。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/90493.html
摘要:主體內(nèi)容區(qū)域小米首頁(yè)下小米商城的主題內(nèi)容區(qū)域,也是整體網(wǎng)頁(yè)面積最廣的區(qū)塊實(shí)在不知道定主體內(nèi)容區(qū)塊時(shí)也可以根據(jù)面積比重來劃分,最大的那塊一定是主題中心,布局的重復(fù)性很高。 單就深入了解布局規(guī)范都足夠說上一個(gè)月的,今天我就不論大范圍,挑選小米網(wǎng)站首頁(yè)的部分區(qū)塊布局來講解吧! 下面是小米官網(wǎng)的首頁(yè),很多人一看到這樣的網(wǎng)頁(yè)就傻眼,不知道咋弄,要么就隨性布局,要么就干看著,其實(shí)遇到問題首先一點(diǎn)就...
摘要:主體內(nèi)容區(qū)域小米首頁(yè)下小米商城的主題內(nèi)容區(qū)域,也是整體網(wǎng)頁(yè)面積最廣的區(qū)塊實(shí)在不知道定主體內(nèi)容區(qū)塊時(shí)也可以根據(jù)面積比重來劃分,最大的那塊一定是主題中心,布局的重復(fù)性很高。 單就深入了解布局規(guī)范都足夠說上一個(gè)月的,今天我就不論大范圍,挑選小米網(wǎng)站首頁(yè)的部分區(qū)塊布局來講解吧! 下面是小米官網(wǎng)的首頁(yè),很多人一看到這樣的網(wǎng)頁(yè)就傻眼,不知道咋弄,要么就隨性布局,要么就干看著,其實(shí)遇到問題首先一點(diǎn)就...
摘要:一般來講,我們的網(wǎng)頁(yè)導(dǎo)航欄是這么個(gè)模式來構(gòu)建在結(jié)構(gòu)上首先我們需要給導(dǎo)航欄的給個(gè)類名一般為然后就是一個(gè)無(wú)序表格由于導(dǎo)航欄的文字一般都是鏈接用來跳轉(zhuǎn)頁(yè)面要在里面包含一個(gè)首頁(yè)云云商城智慧門店?duì)I銷平臺(tái)媒體聯(lián)盟關(guān)于云道在樣式上目前我見過的分為兩種導(dǎo)航一般來講,我們的網(wǎng)頁(yè)導(dǎo)航欄是這么個(gè)模式來構(gòu)建在結(jié)構(gòu)上:1.首先我們需要給導(dǎo)航欄的div 給個(gè)類名 一般為nav2.然后就是一個(gè)無(wú)序表格?3.由于導(dǎo)航欄的文...
閱讀 3420·2021-11-24 09:38
閱讀 3196·2021-11-22 09:34
閱讀 2112·2021-09-22 16:03
閱讀 2373·2019-08-29 18:37
閱讀 383·2019-08-29 16:15
閱讀 1774·2019-08-26 13:56
閱讀 867·2019-08-26 12:21
閱讀 2208·2019-08-26 12:15