JS面試之手寫節(jié)流防抖詳解
前言
作為一個(gè)程序員,代碼實(shí)現(xiàn)才是能力體現(xiàn),在大部分面試的時(shí)候,我們都會(huì)被要求手寫代碼實(shí)現(xiàn)一個(gè)功能,這需要有良好的代碼習(xí)慣和思路,有時(shí)候我們也可以多去看看和理解一些經(jīng)常用到的api源代碼,這是有幫助的。這里我總結(jié)了一下經(jīng)常被面試官問到的節(jié)流和防抖功能的實(shí)現(xiàn),分享給有需要的小伙伴
防抖
防抖,就是防止頻繁重復(fù)的觸發(fā)某個(gè)事件。比如當(dāng)我們點(diǎn)擊一個(gè)按鈕觸發(fā)一個(gè)事件時(shí),可能會(huì)連續(xù)點(diǎn)了幾次,那么會(huì)連續(xù)多次觸發(fā)事件函數(shù),這是一種浪費(fèi),尤其是對(duì)于異步請(qǐng)求數(shù)據(jù),連續(xù)的請(qǐng)求數(shù)據(jù)很大程度會(huì)影響性能和用戶體驗(yàn)。所以,防抖,就是在觸發(fā)一次函數(shù)后規(guī)定時(shí)間內(nèi)沒有再次觸發(fā),再執(zhí)行該事件函數(shù),若多次在規(guī)定時(shí)間內(nèi)重復(fù)觸發(fā),只執(zhí)行最后一次觸發(fā)
實(shí)現(xiàn)思路
首先,我們要知道,防抖函數(shù)是一個(gè)工具函數(shù),我們需要將目標(biāo)事件函數(shù)作為參數(shù)傳入防抖函數(shù),防抖函數(shù)內(nèi)要返回一個(gè)調(diào)用了目標(biāo)函數(shù)的函數(shù),當(dāng)調(diào)用防抖函數(shù)時(shí),讓防抖函數(shù)調(diào)用目標(biāo)事件函數(shù),來實(shí)現(xiàn)防抖功能。其次,假如從一個(gè)中級(jí)程序員角度出發(fā),應(yīng)該想到當(dāng)我們把自己寫的防抖函數(shù)作為一個(gè)api給初級(jí)程序員使用時(shí),希望可以主動(dòng)設(shè)置防抖的時(shí)間間隔,所以這個(gè)時(shí)間間隔也應(yīng)該是防抖函數(shù)的一個(gè)參數(shù)。
function debounce(fn,wait){ return function(){ fn() } }
在這個(gè)防抖函數(shù)內(nèi),我們要調(diào)用這個(gè)傳入的目標(biāo)事件函數(shù),除此之外,我們還要根據(jù)參數(shù)設(shè)置防抖等待的時(shí)間,觸發(fā)一次函數(shù)后,要等待該時(shí)間間隔再執(zhí)行該目標(biāo)事件函數(shù)。所以我們想到使用計(jì)時(shí)器setTimeout()
,調(diào)用目標(biāo)函數(shù),并且設(shè)置等待時(shí)間。當(dāng)我們?cè)诘却龝r(shí)間內(nèi)再次觸發(fā)防抖函數(shù),就需要清除之前的計(jì)時(shí)器,重新等待時(shí)間,那么就需要給定時(shí)器定義變量名,且調(diào)用清除定時(shí)器函數(shù)。
*注:定義定時(shí)器變量名需要在返回的函數(shù)外部,形成閉包,否則每次觸發(fā)防抖函數(shù)調(diào)用目標(biāo)事件函數(shù)會(huì)重新定義一個(gè)同名空變量,導(dǎo)致無法清除之前定時(shí)器
function debounce(fn,wait){ let timeout return function(){ clearTimeout(timeout) timeout = setTimeout(fn,wait) } }
到這里我們已經(jīng)完成了大體功能,之后需要我們考慮更多使用這個(gè)防抖函數(shù)時(shí)可能遇到細(xì)節(jié)情況:
比如定義目標(biāo)事件函數(shù)時(shí),可能會(huì)使用this來指向觸發(fā)這個(gè)事件函數(shù)的DOM結(jié)構(gòu),但是讓該目標(biāo)事件函數(shù)在防抖函數(shù)中的定時(shí)器中調(diào)用時(shí),會(huì)改變?cè)摵瘮?shù)內(nèi)的this指向window,影響到原函數(shù),所以就需要改正調(diào)用該目標(biāo)函數(shù)的this指向。所以可以使用顯示綁定(call、apply、bind)來維護(hù)目標(biāo)事件函數(shù)的this指向。在定時(shí)器調(diào)用目標(biāo)事件函數(shù)時(shí),將該函數(shù)顯示綁定到防抖函數(shù)的this,因?yàn)楫?dāng)防抖函數(shù)被觸發(fā)時(shí),它的this也是指向觸發(fā)這個(gè)函數(shù)的DOM結(jié)構(gòu)
function debounce(fn,wait){ let timeout return function(){ clearTimeout(timeout) timeout = setTimeout(()=>{ //改成箭頭函數(shù),否則不能直接接.call() fn.call(this) },wait) } }
還需要考慮到定義的目標(biāo)事件函數(shù)本身也可能需要傳遞參數(shù),比如可能是事件參數(shù)e,也可能有多個(gè)參數(shù)。所以我們需要解構(gòu)出目標(biāo)事件函數(shù)可能接收的所有函數(shù)。使用防抖函數(shù)調(diào)用目標(biāo)事件函數(shù)時(shí),只能將需要傳遞的參數(shù)傳遞給防抖函數(shù),再?gòu)姆蓝逗瘮?shù)解構(gòu)出參數(shù),來傳遞給目標(biāo)事件函數(shù)。arguments
就是用來代表函數(shù)接受的所有參數(shù),我們就可以用它解構(gòu)防抖函數(shù)接收到的參數(shù)。
*注:arguments
是一個(gè)類數(shù)組,需要將其解構(gòu)出來再變成數(shù)組,然后作為參數(shù)傳遞給目標(biāo)函數(shù)。并且輔助函數(shù)call()
接收參數(shù)的方式是逐個(gè)接收,所以我們需要改成apply()
,來接受數(shù)組作為參數(shù)
function debounce(fn,wait){ let timeout return function(){ let args = [...arguments] clearTimeout(timeout) timeout = setTimeout(()=>{ fn.apply(this,args) },wait) } }
最后,還需要考慮到,定義的目標(biāo)事件函數(shù)可能有返回值,所以我們還需要定義一個(gè)變量來接收目標(biāo)時(shí)間函數(shù)調(diào)用后的返回值,并返回這個(gè)變量
function debounce(fn,wait){ let timeout,result //同樣定義在返回函數(shù)外,形成閉包 return function(){ let args = [...arguments] clearTimeout(timeout) timeout = setTimeout(()=>{ result = fn.apply(this,args) },wait) return result } }
到這里,一個(gè)完整的防抖函數(shù)就實(shí)現(xiàn)了,我們可以嘗試簡(jiǎn)單驗(yàn)證一下,源代碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">0</button> <script> let count = 0 let btn = document.getElementById("btn") function add(){ this.innerHTML = ++count } function debounce(fn,wait){ var timeout,result return function(){ let args = [...arguments] clearTimeout(timeout) timeout = setTimeout(()=>{ result = fn.apply(this,args) },wait) return result } } btn.addEventListener('click',debounce(add,1000)) </script> </body> </html>
節(jié)流
節(jié)流,和防抖類似,都是防止一些不必要的操作,區(qū)別就是,防抖是在規(guī)定時(shí)間內(nèi)一直重復(fù)觸發(fā)函數(shù),就一直不執(zhí)行,直到在規(guī)定時(shí)間內(nèi)沒有再次觸發(fā),才執(zhí)行;而節(jié)流是在規(guī)定時(shí)間內(nèi)一直重復(fù)觸發(fā)函數(shù),那么該規(guī)定時(shí)間內(nèi)只會(huì)執(zhí)行其中一次。比如,當(dāng)點(diǎn)擊觸發(fā)一個(gè)目標(biāo)事件函數(shù),可以直接執(zhí)行這個(gè)函數(shù),但是當(dāng)我們?cè)谝?guī)定時(shí)間內(nèi)又多次重復(fù)觸發(fā)該函數(shù),那么就不執(zhí)行之后的觸發(fā),而規(guī)定時(shí)間之后又有觸發(fā)該函數(shù),就又執(zhí)行該函數(shù),并且從這個(gè)函數(shù)觸發(fā)的時(shí)間開始重新計(jì)算時(shí)間,之后規(guī)定時(shí)間內(nèi)的重復(fù)觸發(fā)也不會(huì)被執(zhí)行,這就是節(jié)流
實(shí)現(xiàn)思路
節(jié)流的實(shí)現(xiàn)思路大體一樣,是一個(gè)工具函數(shù),需要傳入目標(biāo)事件函數(shù)和規(guī)定時(shí)間作為參數(shù),并且返回一個(gè)函數(shù)調(diào)用這個(gè)目標(biāo)事件函數(shù),但是這里不需要使用定時(shí)器setTimeout()
,需要定義一個(gè)開始時(shí)間,當(dāng)點(diǎn)擊觸發(fā)一個(gè)函數(shù)時(shí),將此時(shí)的時(shí)間賦值給開始時(shí)間,之后每次觸發(fā)都判斷此時(shí)距離開始時(shí)間間隔是否超過規(guī)定時(shí)間,若超過就執(zhí)行函數(shù),并且將當(dāng)前時(shí)間又賦值給開始時(shí)間,若未超過,則不執(zhí)行函數(shù)也不重新賦值開始時(shí)間
function throttle(fn,wait){ let preTime = 0 //同樣使用閉包,保留這個(gè)變量 return function(){ let now = +new Date() //一元運(yùn)算符+改為秒數(shù)時(shí)間 if(now - preTime > wait){ fn() preTime = now } } }
另外,同樣需要考慮目標(biāo)事件函數(shù)中的this指向,以及目標(biāo)事件函數(shù)本身需要接收的參數(shù),和可能有返回值得情況。值得注意的是,雖然這里沒有使用setTimeout()
定時(shí)器,但是函數(shù)調(diào)用在返回出來的函數(shù)中,其內(nèi)部this指向也會(huì)被改變,受到影響,所以需要用apply()
輔助函數(shù)顯示綁定,改正this指向
function throttle(fn,wait){ let preTime = 0,result return function(){ let args = [...arguments] let now = +new Date() if(now - preTime > wait){ result = fn.apply(this,args) preTime = now } return result } }
到這里,也完成了節(jié)流函數(shù)的手寫實(shí)現(xiàn),可以測(cè)試一下,源代碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">0</button> <script> let count = 0 let btn = document.getElementById("btn") function add(){ this.innerHTML = ++count } function throttle(fn,wait){ let preTime = 0,result return function(){ let args = [...arguments] let now = +new Date() if(now - preTime > wait){ result = fn.apply(this,args) preTime = now } return result } } btn.addEventListener('click',throttle(add,1000)) </script> </body> </html>
總結(jié)
在面試中,要求手寫實(shí)現(xiàn)功能是經(jīng)常遇到的,而防抖和節(jié)流函數(shù)又是經(jīng)常被要求書寫的函數(shù)之一,大家應(yīng)該也會(huì)覺不難,當(dāng)然寫出來也是幫助有需要的小伙伴,希望大家有所收獲,面試一舉拿下
到此這篇關(guān)于JS面試之手寫節(jié)流防抖詳解的文章就介紹到這了,更多相關(guān)JS節(jié)流防抖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript JSON使用原理及注意事項(xiàng)
這篇文章主要介紹了JavaScript JSON使用原理及注意事項(xiàng),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07Javascript 5種方法實(shí)現(xiàn)過濾刪除前后所有空格
這篇文章主要介紹Javascript 5種過濾刪除前后所有空格的方法,比較實(shí)用,需要的朋友可以參考下。2016-06-06JavaScript擴(kuò)展運(yùn)算符的學(xué)習(xí)及應(yīng)用詳情(ES6)
這篇文章主要介紹了JavaScript擴(kuò)展運(yùn)算符的學(xué)習(xí)及應(yīng)用詳情(ES6),擴(kuò)展運(yùn)算符是ES6新增的一種運(yùn)算符,他可以幫助我們簡(jiǎn)化代碼,簡(jiǎn)化操作,具體相關(guān)知識(shí)感興趣的小伙伴可以查看下面文章的簡(jiǎn)單介紹2022-08-08詳解JavaScript中的構(gòu)造器Constructor模式
構(gòu)造器Constructor不能被繼承,因此不能重寫Overriding,但可以被重載Overloading。通過本文給大家分享JavaScript中的構(gòu)造器Constructor模式,對(duì)構(gòu)造器constructor相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧2016-01-01個(gè)人總結(jié)的一些JavaScript技巧、實(shí)用函數(shù)、簡(jiǎn)潔方法、編程細(xì)節(jié)
這篇文章主要介紹了個(gè)人總結(jié)的一些JavaScript技巧、實(shí)用函數(shù)、簡(jiǎn)潔方法、編程細(xì)節(jié),本文講解了變量轉(zhuǎn)換、取整同時(shí)轉(zhuǎn)換成數(shù)值型、日期轉(zhuǎn)數(shù)值、類數(shù)組對(duì)象轉(zhuǎn)數(shù)組、進(jìn)制之間的轉(zhuǎn)換等方法技巧,需要的朋友可以參考下2015-06-06js+css實(shí)現(xiàn)select的美化效果
這篇文章主要為大家詳細(xì)介紹了js+css實(shí)現(xiàn)select的美化效果,如何針對(duì)select進(jìn)行美化,感興趣的小伙伴們可以參考一下2016-03-03div+css布局的圖片連續(xù)滾動(dòng)js實(shí)現(xiàn)代碼
整理一個(gè)div+css圖片連續(xù)滾動(dòng)代碼,原理跟腳本之家之前發(fā)布的文章一樣。2010-05-05webpack自動(dòng)打包和熱更新的實(shí)現(xiàn)方法
這篇文章主要介紹了webpack自動(dòng)打包和熱更新的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06