詳細(xì)分析JS函數(shù)去抖和節(jié)流
本篇內(nèi)容從節(jié)流和去抖的概念基礎(chǔ)知識(shí)講起,對(duì)JS函數(shù)做了詳細(xì)的分析,一起來(lái)看下:
1、什么是節(jié)流和去抖?
節(jié)流。就是擰緊水龍頭讓水少流一點(diǎn),但是不是不讓水流了。想象一下在現(xiàn)實(shí)生活中有時(shí)候我們需要接一桶水,接水的同時(shí)不想一直站在那等著,可能要離開(kāi)一會(huì)去干一點(diǎn)別的事請(qǐng),讓水差不多流滿一桶水的時(shí)候再回來(lái),這個(gè)時(shí)候,不能把水龍頭開(kāi)的太大,不然還沒(méi)回來(lái)水就已經(jīng)滿了,浪費(fèi)了好多水,這時(shí)候就需要節(jié)流,讓自己回來(lái)的時(shí)候水差不多滿了。那在JS里有沒(méi)有這種情況呢,典型的場(chǎng)景是圖片懶加載監(jiān)聽(tīng)頁(yè)面的scoll事件,或者監(jiān)聽(tīng)鼠標(biāo)的mousemove事件,這些事件對(duì)應(yīng)的處理方法相當(dāng)于水,由于scroll和mousemove在鼠標(biāo)移動(dòng)的時(shí)候會(huì)被瀏覽器頻繁的觸發(fā),會(huì)導(dǎo)致對(duì)應(yīng)的事件也會(huì)被頻繁的觸發(fā)(水流的太快了),這樣就會(huì)造成很大的瀏覽器資源開(kāi)銷(xiāo),而且好多中間的處理是不必要的,這樣就會(huì)造成瀏覽器卡頓的現(xiàn)象,這時(shí)候就需要節(jié)流,如何節(jié)流呢?我們無(wú)法做到讓瀏覽器不去觸發(fā)對(duì)應(yīng)的事件,但是可以做到讓處理事件的方法執(zhí)行頻率減少,從而減少對(duì)應(yīng)的處理開(kāi)銷(xiāo)。
去抖。最早接觸這個(gè)詞應(yīng)該是在高中物理里面學(xué)到的,有時(shí)候開(kāi)關(guān)在在真正閉合之前可能會(huì)發(fā)生一些抖動(dòng)現(xiàn)象,如果抖動(dòng)的明顯的話,對(duì)應(yīng)的小燈泡可能會(huì)閃爍,把燈泡閃壞了不重要,萬(wàn)一把眼睛再給閃壞了可就麻煩了,這個(gè)時(shí)候就有去抖電路的出現(xiàn)。而在我們的頁(yè)面里,也有這種情況,假設(shè)我們的一個(gè)輸入框,輸入內(nèi)容的同時(shí)可能會(huì)去后臺(tái)查詢對(duì)應(yīng)的聯(lián)想 詞,如果用戶輸入的同時(shí),頻繁的觸發(fā)input事件,然后頻繁的向后抬發(fā)送請(qǐng),那么直到用戶輸入完成時(shí),之前的請(qǐng)求都應(yīng)該是多余的,假設(shè)網(wǎng)絡(luò)慢一點(diǎn),后臺(tái)返回的數(shù)據(jù)比較慢,那么顯示的聯(lián)想詞可能會(huì)出現(xiàn)頻繁的變換,直到最后的一個(gè)請(qǐng)求返回。這個(gè)時(shí)候就可以在一定時(shí)間內(nèi)監(jiān)聽(tīng)是否再次輸入,如果沒(méi)有再次輸入則認(rèn)為本次輸入完成,發(fā)送請(qǐng)求,否則就是判定用戶仍在輸入,不發(fā)送請(qǐng)求。
去抖和節(jié)流是不同的,因?yàn)楣?jié)流雖然中間的處理函數(shù)被限制了,但是只是減少了頻率,而去抖則把中間的處理函數(shù)全部過(guò)濾掉了,只執(zhí)行規(guī)判定時(shí)間內(nèi)的最后一個(gè)事件。
2、JS實(shí)現(xiàn)。
前面BB了這么多,感謝你耐心的看到這里,接下來(lái)我們來(lái)自己動(dòng)手看看如何實(shí)現(xiàn)節(jié)流和去抖。
節(jié)流:
/** 實(shí)現(xiàn)思路: ** 參數(shù)需要一個(gè)執(zhí)行的頻率,和一個(gè)對(duì)應(yīng)的處理函數(shù), ** 內(nèi)部需要一個(gè)lastTime 變量記錄上一次執(zhí)行的時(shí)間 **/ function throttle (func, wait) { let lastTime = null // 為了避免每次調(diào)用lastTime都被清空,利用js的閉包返回一個(gè)function確保不生命全局變量也可以 return function () { let now = new Date() // 如果上次執(zhí)行的時(shí)間和這次觸發(fā)的時(shí)間大于一個(gè)執(zhí)行周期,則執(zhí)行 if (now - lastTime - wait > 0) { func() lastTime = now } } }
再看如何調(diào)用:
// 由于閉包的存在,調(diào)用會(huì)不一樣 let throttleRun = throttle(() => { console.log(123) }, 400) window.addEventListener('scroll', throttleRun)
這時(shí)候f瘋狂的滾動(dòng)頁(yè)面,會(huì)發(fā)現(xiàn)會(huì)400ms打印一個(gè)123,而沒(méi)有節(jié)流的話會(huì)不斷地打印, 你可以改變wait參數(shù)去感受下不同。
但是到這里,我們的節(jié)流方法是不完善的,因?yàn)槲覀兊姆椒](méi)有獲取事件發(fā)生時(shí)的this對(duì)象,而且由于我們的方法簡(jiǎn)單粗暴的通過(guò)判斷這次觸發(fā)的時(shí)間和上次執(zhí)行時(shí)間的間隔來(lái)決定是否執(zhí)行回調(diào),這樣就會(huì)造成最后一次觸發(fā)無(wú)法執(zhí)行,或者用戶出發(fā)的間隔確實(shí)很短,也無(wú)法執(zhí)行,造成了誤殺,所以需要對(duì)方法進(jìn)行完善。
function throttle (func, wait) { let lastTime = null let timeout return function () { let context = this let now = new Date() // 如果上次執(zhí)行的時(shí)間和這次觸發(fā)的時(shí)間大于一個(gè)執(zhí)行周期,則執(zhí)行 if (now - lastTime - wait > 0) { // 如果之前有了定時(shí)任務(wù)則清除 if (timeout) { clearTimeout(timeout) timeout = null } func.apply(context, arguments) lastTime = now } else if (!timeout) { timeout = setTimeout(() => { // 改變執(zhí)行上下文環(huán)境 func.apply(context, arguments) }, wait) } } }
這樣我們的方法就相對(duì)完善了,調(diào)用方法和之前相同。
去抖:
去抖的方法,和節(jié)流思路一致,但是只有在抖動(dòng)被判定結(jié)束后,方法才會(huì)得到執(zhí)行。
debounce (func, wait) { let lastTime = null let timeout return function () { let context = this let now = new Date() // 判定不是一次抖動(dòng) if (now - lastTime - wait > 0) { setTimeout(() => { func.apply(context, arguments) }, wait) } else { if (timeout) { clearTimeout(timeout) timeout = null } timeout = setTimeout(() => { func.apply(context, arguments) }, wait) } // 注意這里lastTime是上次的觸發(fā)時(shí)間 lastTime = now } }
這時(shí)候按照之前同樣的方式調(diào)用,會(huì)發(fā)現(xiàn)無(wú)論怎么瘋狂的滾動(dòng)窗口,只有停止?jié)L動(dòng)時(shí),才會(huì)執(zhí)行對(duì)應(yīng)的事件。
去抖和節(jié)流已經(jīng)有很多成熟的js進(jìn)行了實(shí)現(xiàn),其大致思路基本是這樣的。
我們?cè)俳o大家分享一下網(wǎng)友的實(shí)現(xiàn)方法的代碼:
方法一
1.這種實(shí)現(xiàn)方式的思路很好理解:設(shè)置一個(gè)一間隔時(shí)間,比如50毫秒,以此時(shí)間為基準(zhǔn)設(shè)置定時(shí)器,當(dāng)?shù)谝淮斡|發(fā)事件到第二次觸發(fā)事件間隔小于50毫秒時(shí),清除這個(gè)定時(shí)器,并設(shè)置一個(gè)新的定時(shí)器,以此類(lèi)推,直到有一次事件觸發(fā)后50毫秒內(nèi)沒(méi)有重復(fù)觸發(fā)。代碼如下:
function debounce(method){ clearTimeout(method.timer); method.timer=setTimeout(function(){ method(); },50); }
這種設(shè)計(jì)方式有一個(gè)問(wèn)題:本來(lái)應(yīng)該多次觸發(fā)的事件,可能最終只會(huì)發(fā)生一次。具體來(lái)說(shuō),一個(gè)循序漸進(jìn)的滾動(dòng)事件,如果用戶滾動(dòng)太快速,或者程序設(shè)置的函數(shù)節(jié)流間隔時(shí)間太長(zhǎng),那么最終滾動(dòng)事件會(huì)呈現(xiàn)為一個(gè)很突然的跳躍事件,中間過(guò)程都被節(jié)流截掉了。這個(gè)例子舉的有點(diǎn)夸張了,不過(guò)使用這種方式進(jìn)行節(jié)流最終是會(huì)明顯感受到程序比不節(jié)流的時(shí)候“更突?!?,這對(duì)于用戶體驗(yàn)是很差的。有一種彌補(bǔ)這種缺陷的設(shè)計(jì)思路。
方法二
2.第二種實(shí)現(xiàn)方式的思路與第一種稍有差別:設(shè)置一個(gè)間隔時(shí)間,比如50毫秒,以此時(shí)間為基準(zhǔn)穩(wěn)定分隔事件觸發(fā)情況,也就是說(shuō)100毫秒內(nèi)連續(xù)觸發(fā)多次事件,也只會(huì)按照50毫秒一次穩(wěn)定分隔執(zhí)行。代碼如下:
var oldTime=new Date().getTime(); var delay=50; function throttle1(method){ var curTime=new Date().getTime(); if(curTime-oldTime>=delay){ oldTime=curTime; method(); } }
相比于第一種方法,第二種方法也許會(huì)比第一種方法執(zhí)行更多次(有時(shí)候意味著更多次請(qǐng)求后臺(tái),即更多的流量),但是卻很好的解決了第一種方法清除中間過(guò)程的缺陷。因此在具體場(chǎng)景應(yīng)根據(jù)情況擇優(yōu)決定使用哪種方法。
對(duì)于方法二,我們?cè)偬峁┝硪环N同樣功能的寫(xiě)法:
var timer=undefined,delay=50; function throttle2(method){ if(timer){ return ; } method(); timer=setTimeout(function(){ timer=undefined; },delay); }
相關(guān)文章
使用Grunt.js管理你項(xiàng)目的應(yīng)用說(shuō)明
以前我們可能使用NodeJS自己寫(xiě)一個(gè)build程序,但是現(xiàn)在Grunt.js能夠提供我們需要的一切2013-04-04使用JavaScript 實(shí)現(xiàn)各種跨域的方法
本篇文章是對(duì)在JavaScript中實(shí)現(xiàn)各種跨域方法的介紹。需要的朋友參考下2013-05-05JavaScript中的Math.SQRT1_2屬性使用簡(jiǎn)介
這篇文章主要介紹了JavaScript中的Math.SQRT1_2屬性的使用,是JS入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-06-06javascript實(shí)現(xiàn)網(wǎng)頁(yè)屏蔽Backspace事件,輸入框不屏蔽
這篇文章主要介紹了如何實(shí)現(xiàn)網(wǎng)頁(yè)屏蔽Backspace事件而輸入框不屏蔽,需要的朋友可以參考下2015-07-07Ajax responseText解析json數(shù)據(jù)案例詳解
這篇文章主要介紹了Ajax responseText解析json數(shù)據(jù)案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08JavaScript中使用指數(shù)方法Math.exp()的簡(jiǎn)介
這篇文章主要介紹了JavaScript中使用指數(shù)方法Math.exp(),是JS入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-06-06