JavaScript防抖與節(jié)流超詳細(xì)全面講解
1 為什么需要防抖和節(jié)流
在前端開(kāi)發(fā)當(dāng)中,有些交互事件,會(huì)被頻繁觸發(fā),這樣會(huì)導(dǎo)致我們的頁(yè)面渲染性能下降,如果頻繁觸發(fā)接口調(diào)用的話,會(huì)直接導(dǎo)致服務(wù)器性能的浪費(fèi)。
舉個(gè)例子,在下面的代碼中,我們定義了一個(gè)輸入框,輸入一段文字,測(cè)試鍵盤(pán)的keyup(鍵盤(pán)彈起)事件觸發(fā)了多少次,通過(guò)該實(shí)例來(lái)演示事件是如何被頻繁觸發(fā)的。
<input type="text" id="demo"> <div>觸發(fā)了:<span id="count">0</span>次</div> <script> // 獲取input輸入框與span標(biāo)簽 let demo = document.getElementById("demo"); let count = document.getElementById("count"); // 為demo輸入框注冊(cè)keyup事件 let init = 0; // 記錄keyup事件被觸發(fā)的次數(shù) demo.onkeyup = function () { // 將span標(biāo)簽中的文本修改為事件被觸發(fā)的次數(shù) count.innerHTML = ++init; } </script>
從上面的演示可以看到,我在輸入框中輸入了5個(gè)字,但是keyup事件會(huì)被觸發(fā)30次。如果我們使用這樣的方式去檢測(cè)用戶輸入的用戶名是否可用,這樣高頻率的觸發(fā)不僅是對(duì)性能極大的浪費(fèi),而且用戶還沒(méi)有輸入完就開(kāi)始檢測(cè),對(duì)用戶來(lái)說(shuō)提示并不友好。在這樣的情況下,我們就可以等用戶輸入完成之后,再去觸發(fā)函數(shù),這樣的優(yōu)化就使用到了防抖與節(jié)流。
2 防抖與節(jié)流原理
函數(shù)防抖:在事件觸發(fā)后的 n 秒之后,再去執(zhí)行真正需要執(zhí)行的函數(shù),如果在這 n 秒之內(nèi)事件又被觸發(fā),則重新開(kāi)始計(jì)時(shí)。 也就是說(shuō),如果用戶在間隔時(shí)間內(nèi)一直觸發(fā)函數(shù),那么這個(gè)防抖函數(shù)內(nèi)部的真正需要執(zhí)行的函數(shù)將永遠(yuǎn)無(wú)法執(zhí)行。
那么根據(jù)防抖的原理,我們可以嘗試想象一下上面的例子的改進(jìn)措施,如果為keyup事件添加防抖函數(shù),那么只有當(dāng)keyup在一段時(shí)間內(nèi)不再被觸發(fā),函數(shù)才會(huì)執(zhí)行,也就說(shuō)才開(kāi)始計(jì)數(shù)。
函數(shù)節(jié)流:規(guī)定好一個(gè)單位時(shí)間,觸發(fā)函數(shù)一次。如果在這個(gè)單位時(shí)間內(nèi)觸發(fā)多次函數(shù)的話,只有一次是可被執(zhí)行的。想執(zhí)行多次的話,只能等到下一個(gè)周期里。
如果為keyup事件添加節(jié)流函數(shù),那么效果就是,在一段時(shí)間內(nèi),會(huì)計(jì)數(shù)一次,然后在下一段時(shí)間內(nèi),再計(jì)數(shù)一次。
在了解防抖函數(shù)和節(jié)流函數(shù)的原理之后,接下來(lái)我們可以嘗試自己寫(xiě)一個(gè)防抖與節(jié)流的函數(shù),看看是否能達(dá)到我們預(yù)想的效果。
3 實(shí)現(xiàn)一個(gè)防抖函數(shù)
3.1 初步實(shí)現(xiàn)
根據(jù)之前的描述,在事件被觸發(fā)一段時(shí)間之后,函數(shù)才會(huì)執(zhí)行一次,那么防抖函數(shù)中我們應(yīng)該為其傳入兩個(gè)參數(shù):被執(zhí)行的函數(shù)fun
和這段時(shí)間time
。
// fun:被執(zhí)行的函數(shù) // time:間隔的時(shí)間 function debounce(fun, time) { }
對(duì)于防抖函數(shù)來(lái)說(shuō),它的返回值應(yīng)該是一個(gè)函數(shù),因?yàn)槭录|發(fā)時(shí)接收一個(gè)函數(shù)。在該函數(shù)內(nèi)部,要設(shè)計(jì)一個(gè)定時(shí)器,讓在time
時(shí)間后觸發(fā)函數(shù)fun
。
function debounce(fun, time) { return function () { // time時(shí)間后觸發(fā)函數(shù)fun setTimeout(fun, time); } }
但是上面的函數(shù)有一個(gè)問(wèn)題,就是事件再次被觸發(fā)時(shí),會(huì)出現(xiàn)time
時(shí)間后再執(zhí)行一次函數(shù)fun
,不能達(dá)到事件觸發(fā)完成time
時(shí)間后再執(zhí)行函數(shù)的效果,也就是說(shuō),事件會(huì)被延時(shí)觸發(fā),并不能減少觸發(fā),這是因?yàn)槎〞r(shí)器效果進(jìn)行了累加,因此我們需要取消之前的定時(shí)器,以新的定時(shí)器為準(zhǔn)。
function debounce(fun, time) { let timer; return function () { // 取消當(dāng)前的定時(shí)器效果 clearTimeout(timer); // time時(shí)間后觸發(fā)函數(shù)fun timer = setTimeout(fun, time); } }
到這里一個(gè)初步的防抖函數(shù)就完成了,接下來(lái)使用該函數(shù)改進(jìn)之前的例子,具體代碼如下:
<input type="text" id="demo"> <div>觸發(fā)了:<span id="count">0</span>次</div> <script> // 獲取input輸入框與span標(biāo)簽 let demo = document.getElementById("demo"); let count = document.getElementById("count"); // 防抖函數(shù) function debounce(fun, time) { let timer; return function () { // 取消當(dāng)前的定時(shí)器效果 clearTimeout(timer); // time時(shí)間后觸發(fā)函數(shù)fun timer = setTimeout(fun, time); } } // 為demo輸入框注冊(cè)keyup事件 let init = 0; // 記錄keyup事件被觸發(fā)的次數(shù) demo.onkeyup = debounce(function () { // 將span標(biāo)簽中的文本修改為事件被觸發(fā)的次數(shù) count.innerHTML = ++init; }, 1000); </script>
3.2 this問(wèn)題
從上面的效果圖來(lái)看,我輸入5個(gè)字后,1秒后keyup事件就觸發(fā)了1次,對(duì)比之前的30次,大大減少了事件的觸發(fā)頻率。但是添加防抖之后,原本函數(shù)的this指向發(fā)生了改變。原本函數(shù)的this指向了觸發(fā)事件的那個(gè)對(duì)象,但是添加防抖后this指向了window。
// 添加防抖之前打印 this demo.onkeyup = function () { console.log(this); // <input type="text" id="demo"></input> }
// 添加防抖之后打印 this demo.onkeyup = debounce(function () { console.log(this); }, 1000);
因此在防抖函數(shù)中,我們需要重新把this指回觸發(fā)事件的對(duì)象上。那防抖函數(shù)中返回的函數(shù)this指向了誰(shuí)呢,我們可以打印一下:
function debounce(fun, time) { return function () { console.log(this); } }
我們發(fā)現(xiàn)它的this也指向了觸發(fā)事件的對(duì)象,那么接下來(lái)我們只需要讓定時(shí)器的回調(diào)函數(shù)的this指向觸發(fā)事件的對(duì)象就可以,這個(gè)過(guò)程主要使用call
函數(shù)來(lái)修改this的指向。
function debounce(fun, time) { let timer; return function () { // 將當(dāng)前的this賦值給that let that = this; // 取消當(dāng)前的定時(shí)器效果 clearTimeout(timer); // time時(shí)間后觸發(fā)函數(shù)fun timer = setTimeout(function () { fun.call(that); // 使用call改變函數(shù)內(nèi)部的this指向 }, time); } }
3.3 event問(wèn)題
解決了this指向的問(wèn)題,接下來(lái)觀察事件對(duì)象event的內(nèi)容,添加防抖之前,事件對(duì)象event是鍵盤(pán)事件KeyboardEvent,但是添加防抖之后,event為undefined。
// 添加防抖之前 demo.onkeyup = function (e) { console.log(e); }
// 添加防抖后 demo.onkeyup = debounce(function (e) { console.log(e); }, 1000);
同樣的操作,我們可以打印一下防抖函數(shù)返回的函數(shù)的arguments參數(shù),發(fā)現(xiàn)參數(shù)中就包含了事件對(duì)象。
function debounce(fun, time) { console.log(arguments); }
那么接下來(lái)我們將這個(gè)參數(shù)傳給函數(shù)fun
就可以了,具體傳給call函數(shù)。call函數(shù)第二個(gè)參數(shù)開(kāi)始接受其他的參數(shù),因此需要使用spread運(yùn)算符(…)傳遞參數(shù)。
function debounce(fun, time) { let timer; return function () { // 將當(dāng)前的this賦值給that let that = this; // 獲取函數(shù)的參數(shù) let args = arguments; // 取消當(dāng)前的定時(shí)器效果 clearTimeout(timer); // time時(shí)間后觸發(fā)函數(shù)fun timer = setTimeout(function () { // 使用call改變函數(shù)內(nèi)部的this指向,并傳遞參數(shù) fun.call(that, ...args); }, time); } }
3.4 立即執(zhí)行
到這一步防抖函數(shù)基本可以完成了,但是我們可以再為其添加一些功能,比如說(shuō)立即執(zhí)行。當(dāng)設(shè)置了立即執(zhí)行之后,第一次事件觸發(fā)后,函數(shù)fun
會(huì)立即執(zhí)行,但是第一次事件觸發(fā)后的time
時(shí)間后,函數(shù)才可以重新觸發(fā)。
我們可以傳遞第三個(gè)參數(shù),第三個(gè)參數(shù)immediate
決定了是否立即執(zhí)行,true為是,false為否。那么代碼邏輯就可以使用if…else…語(yǔ)句來(lái)進(jìn)行判斷。我們?cè)镜姆蓝逗瘮?shù)肯定不是立即執(zhí)行的,因此放在else語(yǔ)句中。
function debounce(fun, time, immediate) { let timer; return function () { let that = this; // 將當(dāng)前的this賦值給that let args = arguments; // 獲取函數(shù)的參數(shù) clearTimeout(timer); // 取消當(dāng)前的定時(shí)器效果 if (immediate) { // 立即執(zhí)行代碼 } else { // 不立即執(zhí)行 // time時(shí)間后觸發(fā)函數(shù)fun timer = setTimeout(function () { // 使用call改變函數(shù)內(nèi)部的this指向,并傳遞參數(shù) fun.call(that, ...args); }, time); } } }
if語(yǔ)句中的代碼不是簡(jiǎn)單的fun.call(that, ...args);
就可以,因?yàn)楫?dāng)immediate
為true時(shí),就會(huì)一直調(diào)用,與不加防抖沒(méi)什么區(qū)別。因此我們可以引入新的變量callNow,來(lái)記錄是否要立即執(zhí)行。
function debounce(fun, time, immediate) { let timer; return function () { // 將當(dāng)前的this賦值給that let that = this; // 獲取函數(shù)的參數(shù) let args = arguments; // 取消當(dāng)前的定時(shí)器效果 clearTimeout(timer); if (immediate) { // 立即執(zhí)行 let callNow = !timer; timer = setTimeout(function () { timer = null; }, time); if (callNow) fun.call(that, ...args); } else { // 不立即執(zhí)行 // time時(shí)間后觸發(fā)函數(shù)fun timer = setTimeout(function () { // 使用call改變函數(shù)內(nèi)部的this指向,并傳遞參數(shù) fun.call(that, ...args); }, time); } } }
if語(yǔ)句中的具體邏輯為:當(dāng)immediate為true時(shí),如果之前計(jì)時(shí)器不存在,也就是說(shuō)第一次觸發(fā),那么callNow的值為true,那么代碼就會(huì)立即執(zhí)行;計(jì)時(shí)器存在,callNow就是false,不會(huì)立即執(zhí)行代碼。接下來(lái)可以在keyup事件中試驗(yàn)一下:
<input type="text" id="demo"> <div>觸發(fā)了:<span id="count">0</span>次</div> <script> // 獲取input輸入框與span標(biāo)簽 let demo = document.getElementById("demo"); let count = document.getElementById("count"); // 防抖函數(shù) function debounce(fun, time, immediate) { let timer; return function () { // 將當(dāng)前的this賦值給that let that = this; // 獲取函數(shù)的參數(shù) let args = arguments; // 取消當(dāng)前的定時(shí)器效果 clearTimeout(timer); if (immediate) { // 立即執(zhí)行 let callNow = !timer; timer = setTimeout(function () { timer = null; }, time); if (callNow) fun.call(that, ...args); } else { // 不立即執(zhí)行 // time時(shí)間后觸發(fā)函數(shù)fun timer = setTimeout(function () { // 使用call改變函數(shù)內(nèi)部的this指向,并傳遞參數(shù) fun.call(that, ...args); }, time); } } } // 為demo輸入框注冊(cè)keyup事件 let init = 0; // 記錄keyup事件被觸發(fā)的次數(shù) demo.onkeyup = debounce(function () { // 將span標(biāo)簽中的文本修改為事件被觸發(fā)的次數(shù) count.innerHTML = ++init; }, 1000, true); </script>
從上面效果可以看出,在輸入第一個(gè)1時(shí),事件就立即觸發(fā)了,在接下來(lái)的1秒內(nèi)事件不再被觸發(fā),而是在事件被觸發(fā)的1秒之后才可以繼續(xù)觸發(fā)。
3.5 返回值問(wèn)題
如果被執(zhí)行的函數(shù)有返回值,使用上面的防抖函數(shù)就沒(méi)辦法獲取到返回值了,因此可以繼續(xù)改進(jìn):
function debounce(fun, time, immediate) { // result用來(lái)獲取返回值 let timer, result; return function () { // 將當(dāng)前的this賦值給that let that = this; // 獲取函數(shù)的參數(shù) let args = arguments; // 取消當(dāng)前的定時(shí)器效果 clearTimeout(timer); if (immediate) { // 立即執(zhí)行 let callNow = !timer; timer = setTimeout(function () { timer = null; }, time); if (callNow) result = fun.call(that, ...args); } else { // 不立即執(zhí)行 // time時(shí)間后觸發(fā)函數(shù)fun timer = setTimeout(function () { // 使用call改變函數(shù)內(nèi)部的this指向,并傳遞參數(shù) fun.call(that, ...args); }, time); } return result; } }
3.6 取消防抖
如果一個(gè)防抖函數(shù)等待的時(shí)間過(guò)長(zhǎng),immediate為true,那么我們可以取消防抖,然后再去觸發(fā),這樣就可以減少等待時(shí)間。
在代碼中我們將防抖返回的函數(shù)保存在變量debounced
中,并且為它增加一個(gè)cancel方法,通過(guò)該方法可以取消當(dāng)前的定時(shí)器,從而實(shí)現(xiàn)取消的效果。
function debounce(fun, time, immediate) { // result用來(lái)獲取返回值 let timer, result; let debounced = function () { // 將當(dāng)前的this賦值給that let that = this; // 獲取函數(shù)的參數(shù) let args = arguments; // 取消當(dāng)前的定時(shí)器效果 clearTimeout(timer); if (immediate) { // 立即執(zhí)行 let callNow = !timer; timer = setTimeout(function () { timer = null; }, time); if (callNow) result = fun.call(that, ...args); } else { // 不立即執(zhí)行 // time時(shí)間后觸發(fā)函數(shù)fun timer = setTimeout(function () { // 使用call改變函數(shù)內(nèi)部的this指向,并傳遞參數(shù) fun.call(that, ...args); }, time); } return result; } debounced.cancel = function () { clearTimeout(timer); // 清除定時(shí)器 timer = null; // 閉包會(huì)導(dǎo)致內(nèi)存泄漏,因此需要將定時(shí)器制空 } return debounced; // 返回防抖函數(shù) }
使用keyup事件試驗(yàn)一下,當(dāng)沒(méi)有取消防抖時(shí),一段時(shí)間后才可以再次觸發(fā)事件:
<input type="text" id="demo"> <div>觸發(fā)了:<span id="count">0</span>次</div> <button id="btn">取消防抖</button> <script> // 獲取input輸入框、span標(biāo)簽、按鈕 let demo = document.getElementById("demo"); let count = document.getElementById("count"); let btn = document.getElementById("btn"); // 防抖代碼函數(shù)省略 // 為demo輸入框注冊(cè)keyup事件 let init = 0; // 記錄keyup事件被觸發(fā)的次數(shù) function fun() { // 觸發(fā)keyup后要執(zhí)行的函數(shù) count.innerHTML = ++init; } let fd = debounce(fun, 3000, true); demo.onkeyup = fd; // 為輸入框注冊(cè)keyup事件 btn.onclick = function () { // 取消防抖的效果 fd.cancel(); } </script>
當(dāng)取消防抖函數(shù)之后,就可以立即觸發(fā)事件了:
3.7 總結(jié)
初步防抖函數(shù),解決了this指向以及event參數(shù)的問(wèn)題:
function debounce(fun, time) { let timer; return function () { // 將當(dāng)前的this賦值給that let that = this; // 獲取函數(shù)的參數(shù) let args = arguments; // 取消當(dāng)前的定時(shí)器效果 clearTimeout(timer); // time時(shí)間后觸發(fā)函數(shù)fun timer = setTimeout(function () { // 使用call改變函數(shù)內(nèi)部的this指向,并傳遞參數(shù) fun.call(that, ...args); }, time); } }
增加了立即執(zhí)行效果的防抖函數(shù):
function debounce(fun, time, immediate) { let timer; return function () { // 將當(dāng)前的this賦值給that let that = this; // 獲取函數(shù)的參數(shù) let args = arguments; // 取消當(dāng)前的定時(shí)器效果 clearTimeout(timer); if (immediate) { // 立即執(zhí)行 let callNow = !timer; timer = setTimeout(function () { timer = null; }, time); if (callNow) fun.call(that, ...args); } else { // 不立即執(zhí)行 // time時(shí)間后觸發(fā)函數(shù)fun timer = setTimeout(function () { // 使用call改變函數(shù)內(nèi)部的this指向,并傳遞參數(shù) fun.call(that, ...args); }, time); } } }
解決了返回值問(wèn)題的防抖函數(shù):
function debounce(fun, time, immediate) { // result用來(lái)獲取返回值 let timer, result; return function () { // 將當(dāng)前的this賦值給that let that = this; // 獲取函數(shù)的參數(shù) let args = arguments; // 取消當(dāng)前的定時(shí)器效果 clearTimeout(timer); if (immediate) { // 立即執(zhí)行 let callNow = !timer; timer = setTimeout(function () { timer = null; }, time); if (callNow) result = fun.call(that, ...args); } else { // 不立即執(zhí)行 // time時(shí)間后觸發(fā)函數(shù)fun timer = setTimeout(function () { // 使用call改變函數(shù)內(nèi)部的this指向,并傳遞參數(shù) fun.call(that, ...args); }, time); } return result; } }
增加了取消功能的防抖函數(shù):
function debounce(fun, time, immediate) { // result用來(lái)獲取返回值 let timer, result; let debounced = function () { // 將當(dāng)前的this賦值給that let that = this; // 獲取函數(shù)的參數(shù) let args = arguments; // 取消當(dāng)前的定時(shí)器效果 clearTimeout(timer); if (immediate) { // 立即執(zhí)行 let callNow = !timer; timer = setTimeout(function () { timer = null; }, time); if (callNow) result = fun.call(that, ...args); } else { // 不立即執(zhí)行 // time時(shí)間后觸發(fā)函數(shù)fun timer = setTimeout(function () { // 使用call改變函數(shù)內(nèi)部的this指向,并傳遞參數(shù) fun.call(that, ...args); }, time); } return result; } debounced.cancel = function () { clearTimeout(timer); // 清除定時(shí)器 timer = null; // 閉包會(huì)導(dǎo)致內(nèi)存泄漏,因此需要將定時(shí)器制空 } return debounced; // 返回防抖函數(shù) }
4 實(shí)現(xiàn)節(jié)流函數(shù)
4.1 通過(guò)時(shí)間戳實(shí)現(xiàn)節(jié)流
當(dāng)觸發(fā)事件的時(shí)候,我們?nèi)〕霎?dāng)前的時(shí)間戳,然后減去之前的時(shí)間戳(時(shí)間戳初始值為0),如果大于設(shè)置的時(shí)間time
,就執(zhí)行函數(shù)fun
,然后更新時(shí)間戳為當(dāng)前的時(shí)間戳,如果小于time
,就不執(zhí)行函數(shù)。
根據(jù)上面的表述,節(jié)流函數(shù)有兩個(gè)參數(shù),一個(gè)是要執(zhí)行的函數(shù)fun
,一個(gè)是等待的時(shí)間time
,那么就可以寫(xiě)出初始的代碼:
function throttle(fun, time) { // 節(jié)流代碼 }
節(jié)流函數(shù)的返回值也是一個(gè)函數(shù),首先要設(shè)置初始時(shí)間戳為0,然后獲取當(dāng)前的時(shí)間戳,如果間隔的時(shí)間大于time
,那么就執(zhí)行函數(shù),否則不執(zhí)行。
function throttle(fun, time) { let old = 0; return function () { let now = new Date().valueOf(); // 獲取當(dāng)前的時(shí)間戳 if (now - old > time) { fun(); // 執(zhí)行函數(shù) old = now; // 更新舊時(shí)間戳 } } }
與防抖函數(shù)相同,要考慮到this指向和event改變的情況,因此引入兩個(gè)變量,使用call函數(shù)更改this指向并且重新傳入?yún)?shù)。
function throttle(fun, time) { let that, args; let old = 0; return function () { let now = new Date().valueOf(); // 獲取當(dāng)前的時(shí)間戳 that = this; // 獲取this args = arguments; // 獲取參數(shù) if (now - old > time) { fun.apply(that, args); // 更改this指向并傳入?yún)?shù) old = now; // 更新舊時(shí)間戳 } } }
示例代碼:節(jié)流函數(shù)效果
<input type="text" id="demo"> <div>觸發(fā)了:<span id="count">0</span>次</div> <script> // 獲取input輸入框、span標(biāo)簽 let demo = document.getElementById("demo"); let count = document.getElementById("count"); // 節(jié)流函數(shù) function throttle(fun, time) { let that, args; let old = 0; return function () { let now = new Date().valueOf(); // 獲取當(dāng)前的時(shí)間戳 that = this; // 獲取this args = arguments; // 獲取參數(shù) if (now - old > time) { fun.apply(that, args); // 更改this指向并傳入?yún)?shù) old = now; // 更新舊時(shí)間戳 } } } // 為demo輸入框注冊(cè)keyup事件 let init = 0; // 記錄keyup事件被觸發(fā)的次數(shù) demo.onkeyup = throttle(function () { count.innerHTML = ++init; }, 1000); </script>
4.2 使用定時(shí)器實(shí)現(xiàn)節(jié)流
節(jié)流函數(shù)有兩個(gè)參數(shù),并且返回值是一個(gè)函數(shù),會(huì)修改this指向和event事件對(duì)象,那么它的框架就可以理出來(lái)了:
function throttle(fun, time) { let that, args; return function () { that = this; args = arguments; fun.apply(that, args); } }
函數(shù)應(yīng)該在定時(shí)器的回調(diào)函數(shù)中調(diào)用,因此還需要聲明一個(gè)定時(shí)器變量timer
,當(dāng)定時(shí)器不存在時(shí),觸發(fā)定時(shí)器,調(diào)用函數(shù)。
function throttle(fun, time) { // timer是定時(shí)器對(duì)象 let that, args, timer; return function () { that = this; args = arguments; if (!timer) { timer = setTimeout(function () { fun.apply(that, args); }, time); } } }
但是當(dāng)定時(shí)器timer
一旦觸發(fā),就會(huì)永遠(yuǎn)有值,不可能再觸發(fā)定時(shí)器了,因此需要在定時(shí)器回調(diào)函數(shù)中將time
置為空。
function throttle(fun, time) { // timer是定時(shí)器對(duì)象 let that, args, timer; return function () { that = this; args = arguments; if (!timer) { timer = setTimeout(function () { timer = null; fun.apply(that, args); }, time); } } }
示例代碼:查看函數(shù)效果
<input type="text" id="demo"> <div>觸發(fā)了:<span id="count">0</span>次</div> <script> // 獲取input輸入框、span標(biāo)簽、按鈕 let demo = document.getElementById("demo"); let count = document.getElementById("count"); // 節(jié)流函數(shù) function throttle(fun, time) { // timer是定時(shí)器對(duì)象 let that, args, timer; return function () { that = this; args = arguments; if (!timer) { timer = setTimeout(function () { timer = null; fun.apply(that, args); }, time); } } } // 為demo輸入框注冊(cè)keyup事件 let init = 0; // 記錄keyup事件被觸發(fā)的次數(shù) demo.onkeyup = throttle(function () { count.innerHTML = ++init; }, 1000); </script>
4.3 時(shí)間戳和定時(shí)器組合實(shí)現(xiàn)
從上面的效果可以看出,時(shí)間戳?xí)r間的節(jié)流函數(shù),第一次輸入文本會(huì)立即觸發(fā),但是當(dāng)輸入結(jié)束后就不再觸發(fā)了,定時(shí)器實(shí)現(xiàn)的節(jié)流函數(shù),第一次輸入文本要等待一段時(shí)間后再觸發(fā),但是當(dāng)輸入結(jié)束之后還會(huì)再觸發(fā)一遍。那么接下來(lái)實(shí)現(xiàn)一個(gè)第一次輸入文本會(huì)立即觸發(fā),但是輸入結(jié)束之后還會(huì)再次觸發(fā)的節(jié)流函數(shù)。
首先將兩個(gè)防抖函數(shù)合并一下:
function throttle(fun, time) { let that, args, timer; let old = 0; // 設(shè)置初始時(shí)間戳 return function () { that = this; args = arguments; let now = new Date().valueOf(); // 獲取初始的時(shí)間戳 if (now - old > time) { fun.apply(that, args); old = now; } if (!timer) { timer = setTimeout(function () { timer = null; fun.apply(that, args); }, time); } } }
這個(gè)防抖函數(shù)的時(shí)間戳和定時(shí)器同時(shí)在運(yùn)行,那我們可以在定時(shí)器時(shí)間戳內(nèi)部,定時(shí)器內(nèi)回調(diào)函數(shù)執(zhí)行一次,就將old
的值設(shè)置為最新的時(shí)間戳。這樣就可以讓時(shí)間戳和定時(shí)器節(jié)流函數(shù)時(shí)間同步。
function throttle(fun, time) { let that, args, timer; let old = 0; // 設(shè)置初始時(shí)間戳 return function () { that = this; args = arguments; let now = new Date().valueOf(); // 獲取初始的時(shí)間戳 if (now - old > time) { fun.apply(that, args); old = now; } if (!timer) { timer = setTimeout(function () { old = new Date().valueOf(); // 將old的值設(shè)置為最新的時(shí)間戳 timer = null; fun.apply(that, args); }, time); } } }
但是不應(yīng)該讓兩個(gè)同步,接下來(lái)就執(zhí)行定時(shí)器的防抖函數(shù),在時(shí)間戳防抖函數(shù)中把定時(shí)器取消掉置為空。
function throttle(fun, time) { let that, args, timer; let old = 0; // 設(shè)置初始時(shí)間戳 return function () { that = this; args = arguments; let now = new Date().valueOf(); // 獲取初始的時(shí)間戳 if (now - old > time) { if (timer) { clearTimeout(timer); timer = null; } fun.apply(that, args); old = now; } if (!timer) { timer = setTimeout(function () { old = new Date().valueOf(); // 將old的值設(shè)置為最新的時(shí)間戳 timer = null; fun.apply(that, args); }, time); } } }
示例代碼:查看節(jié)流函數(shù)的效果
<input type="text" id="demo"> <div>觸發(fā)了:<span id="count">0</span>次</div> <script> // 獲取input輸入框、span標(biāo)簽 let demo = document.getElementById("demo"); let count = document.getElementById("count"); // 節(jié)流函數(shù)省略 // 為demo輸入框注冊(cè)keyup事件 let init = 0; // 記錄keyup事件被觸發(fā)的次數(shù) demo.onkeyup = throttle(function () { count.innerHTML = ++init; }, 1000); </script>
4.4 節(jié)流優(yōu)化
如果我們希望設(shè)計(jì)一個(gè)防抖函數(shù),可以根據(jù)不同的情況來(lái)選擇不同的防抖函數(shù),也就是說(shuō),對(duì)上面三種情況再進(jìn)行一個(gè)結(jié)合。那么我們可以設(shè)置options
為第三個(gè)參數(shù),根據(jù)傳的值判斷使用哪種防抖函數(shù)。options
可以有兩個(gè)參數(shù):leading
,表示是否打開(kāi)第一次執(zhí)行;trailing
:表示是否打開(kāi)最后一次執(zhí)行。
function throttle(fun, time, options) { // options決定使用哪種節(jié)流效果 let that, args, timer; let old = 0; // 設(shè)置初始時(shí)間戳 if (!options) options = {}; // 如果沒(méi)有該參數(shù),置為空對(duì)象 return function () { that = this; args = arguments; let now = new Date().valueOf(); // 獲取初始的時(shí)間戳 // leading為false,表示不打開(kāi)第一次執(zhí)行 if (options.leading === false && !old) { old = now; // 這樣會(huì)將下面的時(shí)間戳節(jié)流代碼跳過(guò) } if (now - old > time) { // 第一次回直接執(zhí)行 if (timer) { clearTimeout(timer); timer = null; } fun.apply(that, args); old = now; } // trailing為false,表示不打開(kāi)最后一次執(zhí)行 if (!timer && options.trailing !== false) { // 最后一次會(huì)被執(zhí)行 timer = setTimeout(function () { old = new Date().valueOf(); // 將old的值設(shè)置為最新的時(shí)間戳 timer = null; fun.apply(that, args); }, time); } } }
示例代碼:節(jié)流函數(shù)的使用效果,打開(kāi)第一次執(zhí)行和最后一次執(zhí)行
demo.onkeyup = throttle(function () { count.innerHTML = ++init; }, 1000, { leading: true, trailing: true }); // 表示打開(kāi)第一次執(zhí)行和最后一次執(zhí)行
打開(kāi)第一次執(zhí)行,關(guān)閉最后一次執(zhí)行:
demo.onkeyup = throttle(function () { count.innerHTML = ++init; }, 1000, { leading: true, trailing: false }); // 表示打開(kāi)第一次執(zhí)行,關(guān)閉最后一次執(zhí)行
關(guān)閉第一次執(zhí)行,打開(kāi)最后一次執(zhí)行:
demo.onkeyup = throttle(function () { count.innerHTML = ++init; }, 1000, { leading: false, trailing: true }); // 表示打開(kāi)第一次執(zhí)行,關(guān)閉最后一次執(zhí)行
如果兩個(gè)都關(guān)閉,會(huì)出現(xiàn)bug,因此使用時(shí)不會(huì)將兩個(gè)都關(guān)閉。
5 應(yīng)用場(chǎng)景
防抖應(yīng)用場(chǎng)景:
- scroll事件滾動(dòng)觸發(fā)
- 搜索框輸入查詢
- 表單驗(yàn)證
- 按鈕提交事件
- 瀏覽器窗口縮放,resize事件
節(jié)流應(yīng)用場(chǎng)景:
- DOM元素的拖拽功能實(shí)現(xiàn)
- 射擊游戲
- 計(jì)算鼠標(biāo)的移動(dòng)距離
- 監(jiān)聽(tīng)scroll滾動(dòng)事件
本文學(xué)習(xí)于視頻:手寫(xiě)函數(shù)防抖和節(jié)流
到此這篇關(guān)于JavaScript防抖與節(jié)流超詳細(xì)全面講解的文章就介紹到這了,更多相關(guān)JavaScript防抖與節(jié)流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 一文學(xué)會(huì)JavaScript如何手寫(xiě)防抖節(jié)流
- 一文教你徹底學(xué)會(huì)JavaScript手寫(xiě)防抖節(jié)流
- 利用JavaScript實(shí)現(xiàn)防抖節(jié)流函數(shù)的示例代碼
- JavaScript函數(shù)防抖與函數(shù)節(jié)流的定義及使用詳解
- JS中節(jié)流和防抖函數(shù)的實(shí)現(xiàn)及區(qū)別示例
- JavaScript防抖動(dòng)與節(jié)流處理
- JavaScript中防抖和節(jié)流的區(qū)別及適用場(chǎng)景
- Vue手寫(xiě)防抖和節(jié)流函數(shù)代碼詳解
相關(guān)文章
js動(dòng)態(tài)修改整個(gè)頁(yè)面樣式達(dá)到換膚效果
這篇文章主要介紹了通過(guò)js動(dòng)態(tài)修改整個(gè)頁(yè)面樣式達(dá)到換膚效果,需要的朋友可以參考下2014-05-05用JavaScript實(shí)現(xiàn) 鐵甲無(wú)敵獎(jiǎng)門(mén)人 “開(kāi)口中”猜數(shù)游戲
JavaScript在常人看來(lái)都是門(mén)出不了廳堂的小語(yǔ)言,僅管它沒(méi)有明星語(yǔ)言的閃耀,但至少網(wǎng)頁(yè)的閃耀還是需要它的,同時(shí)它是一門(mén)很實(shí)用的語(yǔ)言。2009-10-10javascript css float屬性的特殊寫(xiě)法
使用js操作css屬性的寫(xiě)法是有一定的規(guī)律的2008-11-11js對(duì)象屬性名駝峰式轉(zhuǎn)下劃線的實(shí)例代碼
這篇文章主要介紹了js對(duì)象屬性名駝峰式轉(zhuǎn)下劃線的實(shí)例代碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09js實(shí)現(xiàn)開(kāi)關(guān)燈效果
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)開(kāi)關(guān)燈效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10在JS中a標(biāo)簽加入單擊事件屏蔽href跳轉(zhuǎn)頁(yè)面
這篇文章主要介紹了JS中a標(biāo)簽加入單擊事件屏蔽href跳轉(zhuǎn)頁(yè)面的相關(guān)資料,需要的朋友可以參考下2016-12-12