前端百題斬—通俗易懂的防抖與節(jié)流
性能一直是前端老生常談的一個(gè)話題,其中有一個(gè)性能問題就是我們會(huì)頻繁的觸發(fā)一些事件,例如mousemove、scroll、resize等,雖然瀏覽器已經(jīng)對(duì)這些事件的觸發(fā)做了一些優(yōu)化,但是如果在很短的時(shí)間內(nèi)頻繁的觸發(fā)仍然會(huì)影響性能,這個(gè)時(shí)候就需要今天的主角:防抖和節(jié)流,利用它們來(lái)進(jìn)行優(yōu)化,提高性能。
1 防抖
1.1 定義
防抖就是將多次高頻操作優(yōu)化為只在最后一次執(zhí)行(某個(gè)函數(shù)在某段時(shí)間內(nèi),無(wú)論觸發(fā)了多少次回調(diào),都只執(zhí)行最后一次)。通常的使用場(chǎng)景是:用戶輸入,只需在輸入完成后做一次輸入校驗(yàn)即可。
1.2 實(shí)現(xiàn)
防抖是將多次操作合并為一次操作完成,其原理就是維護(hù)一個(gè)計(jì)時(shí)器,在規(guī)定的時(shí)間后觸發(fā)函數(shù),但是在該規(guī)定時(shí)間內(nèi)再次觸發(fā)的話就會(huì)取消之前的定時(shí)器而重新設(shè)置,從而保證了只有最后一次操作能夠被觸發(fā)。其實(shí)現(xiàn)步驟如下所示:
- 利用閉包保存一個(gè)timer變量,然后返回一個(gè)函數(shù)(這個(gè)返回的函數(shù)就是后續(xù)頻繁觸發(fā)操作中調(diào)用的函數(shù));
- 根據(jù)標(biāo)志位判斷是否第一次需要立即執(zhí)行(因?yàn)橛行┣闆r是需要首次調(diào)用函數(shù)立即執(zhí)行的,若沒有該參數(shù),就會(huì)在定時(shí)器到了之后才會(huì)執(zhí)行);
- 當(dāng)有新的觸發(fā)時(shí),若存在定時(shí)器,則清空該定時(shí)器;
- 設(shè)定一個(gè)新的定時(shí)器,重新計(jì)時(shí)。
- function debounce(fn, wait, immediate) {
- let timer = null;
- return function (...args) {
- // 立即執(zhí)行的功能(timer為空表示首次觸發(fā))
- if (immediate && !timer) {
- fn.apply(this, args);
- }
- // 有新的觸發(fā),則把定時(shí)器清空
- timer && clearTimeout(timer);
- // 重新計(jì)時(shí)
- timer = setTimeout(() => {
- fn.apply(this, args);
- }, wait);
- }
- }
1.3 效果預(yù)覽

觀察效果圖可以驗(yàn)證上述的理論知識(shí):
- 防抖之后輸出內(nèi)容的頻次降低了;
- 防抖之后,其在超過一定時(shí)間之后才會(huì)輸出內(nèi)容。
2 節(jié)流
2.1 定義
節(jié)流就是每隔一段時(shí)間后執(zhí)行一次,也就是降低頻率,將高頻操作優(yōu)化成低頻操作。通常使用場(chǎng)景:滾動(dòng)條事件、resize事件、動(dòng)畫等,通常每隔100-500ms執(zhí)行一次即可。
2.2 實(shí)現(xiàn)
節(jié)流函數(shù)的實(shí)現(xiàn)方式有兩種:定時(shí)器版本、時(shí)間戳版本,這兩者各有千秋,下面來(lái)簡(jiǎn)要實(shí)現(xiàn)一下。
2.2.1 定時(shí)器版本
定時(shí)器版本的節(jié)流函數(shù)其重點(diǎn)是利用閉包保存timer變量,具有兩個(gè)特點(diǎn):
- n秒后才會(huì)執(zhí)行第一次(定時(shí)器到了時(shí)間后才會(huì)觸發(fā));
- 停止觸發(fā)后節(jié)流函數(shù)還會(huì)執(zhí)行一次(因?yàn)樵摵瘮?shù)是延遲執(zhí)行的,當(dāng)停止觸發(fā)時(shí)其任務(wù)已經(jīng)到了隊(duì)列中,所以停止后還會(huì)執(zhí)行一次)。
- // 定時(shí)器版本
- function throttle(fn, wait) {
- let timer = null;
- return function(...args) {
- if (!timer) {
- timer = setTimeout(() => {
- fn.apply(this, args);
- timer = null;
- }, wait)
- }
- }
- }
2.2.2 時(shí)間戳版本
時(shí)間戳版本的節(jié)流函數(shù)重點(diǎn)是利用閉包保存上一次的時(shí)間previous,具有兩個(gè)特點(diǎn):
開始觸發(fā)后會(huì)立即執(zhí)行(因?yàn)閜revious開始會(huì)被賦值為0);
停止觸發(fā)后不再執(zhí)行(因?yàn)樵摵瘮?shù)是同步任務(wù),在觸發(fā)的時(shí)候就會(huì)進(jìn)行相應(yīng)的判斷,所以就不存在停止觸發(fā)后再執(zhí)行的情況)。
- // 時(shí)間戳版本
- function throttle(fn, wait) {
- // 上一次執(zhí)行時(shí)間
- let previous = 0;
- return function(...args) {
- // 當(dāng)前時(shí)間
- let now = +new Date();
- if (now - previous > wait) {
- previous = now;
- fn.apply(this, args);
- }
- }
- }
2.3 效果預(yù)覽
觀察效果圖可以驗(yàn)證上述的理論知識(shí):
- 節(jié)流確實(shí)降低了內(nèi)容的輸出頻率,將高頻變?yōu)榈皖l;
- 時(shí)間戳版本的節(jié)流函數(shù)在首次會(huì)輸出內(nèi)容,但是最后一次的內(nèi)容不會(huì)輸出(謹(jǐn)慎使用);
- 定時(shí)器版本的節(jié)流函數(shù)確實(shí)不會(huì)立刻打印內(nèi)容,而是超過一定時(shí)間之后才會(huì)打印;此外,其最后輸入的內(nèi)容會(huì)被打印出來(lái)。