十分鐘學(xué)會防抖和節(jié)流

作為一名前端開發(fā)者,我們經(jīng)常會處理各種事件蔑赘,比如常見的click狸驳、scroll、 resize等等缩赛。仔細(xì)一想耙箍,會發(fā)現(xiàn)像scroll、scroll酥馍、onchange這類事件會頻繁觸發(fā)辩昆,如果我們在回調(diào)中計(jì)算元素位置、做一些跟DOM相關(guān)的操作旨袒,引起瀏覽器回流和重繪汁针,頻繁觸發(fā)回調(diào),很可能會造成瀏覽器掉幀砚尽,甚至?xí)篂g覽器崩潰施无,影響用戶體驗(yàn)。針對這種現(xiàn)象必孤,目前有兩種常用的解決方案:防抖和節(jié)流帆精。

防抖(debounce)

所謂防抖,就是指觸發(fā)事件后隧魄,就是把觸發(fā)非常頻繁的事件合并成一次去執(zhí)行卓练。即在指定時(shí)間內(nèi)只執(zhí)行一次回調(diào)函數(shù),如果在指定的時(shí)間內(nèi)又觸發(fā)了該事件购啄,則回調(diào)函數(shù)的執(zhí)行時(shí)間會基于此刻重新開始計(jì)算襟企。

以我們生活中乘車刷卡的情景舉例,只要乘客不斷地在刷卡狮含,司機(jī)師傅就不能開車顽悼,乘客刷卡完畢之后曼振,司機(jī)會等待幾分鐘,確定乘客坐穩(wěn)再開車蔚龙。如果司機(jī)在最后等待的時(shí)間內(nèi)又有新的乘客上車冰评,那么司機(jī)等乘客刷卡完畢之后,還要再等待一會木羹,等待所有乘客坐穩(wěn)再開車甲雅。


image.png

具體應(yīng)該怎么去實(shí)現(xiàn)這樣的功能呢嘱丢?第一時(shí)間肯定會想到使用setTimeout方法侣诺,那我們就嘗試寫一個(gè)簡單的函數(shù)來實(shí)現(xiàn)這個(gè)功能吧~

var debounce = function(fn, delayTime) {
var timeId;  
return function() {    
var context = this, args = arguments;
    timeId && clearTimeout(timeId);
    timeId = setTimeout(function(){
      fn.apply(context, args);
    }, delayTime)
  }
}

思路解析:

執(zhí)行debounce函數(shù)之后會返回一個(gè)新的函數(shù),通過閉包的形式糜芳,維護(hù)一個(gè)變量timeId脐瑰,每次執(zhí)行該函數(shù)的時(shí)候會結(jié)束之前的延遲操作妖枚,重新執(zhí)行setTimeout方法,也就實(shí)現(xiàn)了上面所說的指定的時(shí)間內(nèi)多次觸發(fā)同一個(gè)事件苍在,會合并執(zhí)行一次绝页。

溫馨提示:

1、上述代碼中arguments只會保存事件回調(diào)函數(shù)中的參數(shù)寂恬,譬如:事件對象等抒寂,并不會保存fn、delayTime

2掠剑、使用apply改變傳入的fn方法中的this指向,指向綁定事件的DOM元素郊愧。

節(jié)流(throttle)

所謂節(jié)流朴译,是指頻繁觸發(fā)事件時(shí),只會在指定的時(shí)間段內(nèi)執(zhí)行事件回調(diào)属铁,即觸發(fā)事件間隔大于等于指定的時(shí)間才會執(zhí)行回調(diào)函數(shù)眠寿。

類比到生活中的水龍頭,擰緊水龍頭到某種程度會發(fā)現(xiàn)焦蘑,每隔一段時(shí)間盯拱,就會有水滴流出。

image.png

說到時(shí)間間隔例嘱,大家肯定會想到使用setTimeout來實(shí)現(xiàn)狡逢,在這里,我們使用兩種方法來簡單實(shí)現(xiàn)這種功能:時(shí)間戳和setTimeout定時(shí)器拼卵。

時(shí)間戳

var throttle = (fn, delayTime) => {  
var _start = Date.now();  
return function() {    
var _now = Date.now(), context = this, args = arguments;    
if(_now - _start >= delayTime) {
      fn.apply(context, args);
      _start = Date.now();
    }
  }
}

通過比較兩次時(shí)間戳的間隔是否大于等于我們事先指定的時(shí)間來決定是否執(zhí)行事件回調(diào)奢浑。

定時(shí)器

var throttle = function(fn, delayTime) {  
var flag;  
return function() {    
var context = this, args = arguments;    
if(!flag) {
      flag = setTimeout(function() {
        fn.apply(context, args);
        flag = false;
      }, delayTime);
    }
  }
}

在上述實(shí)現(xiàn)過程中,我們設(shè)置了一個(gè)標(biāo)志變量flag腋腮,當(dāng)delayTime之后執(zhí)行事件回調(diào)雀彼,便會把這個(gè)變量重置壤蚜,表示一次回調(diào)已經(jīng)執(zhí)行結(jié)束。 對比上述兩種實(shí)現(xiàn)徊哑,我們會發(fā)現(xiàn)一個(gè)有趣的現(xiàn)象:

1袜刷、使用時(shí)間戳方式,頁面加載的時(shí)候就會開始計(jì)時(shí)莺丑,如果頁面加載時(shí)間大于我們設(shè)定的delayTime著蟹,第一次觸發(fā)事件回調(diào)的時(shí)候便會立即fn,并不會延遲窒盐。如果最后一次觸發(fā)回調(diào)與前一次觸發(fā)回調(diào)的時(shí)間差小于delayTime草则,則最后一次觸發(fā)事件并不會執(zhí)行fn;

2蟹漓、使用定時(shí)器方式炕横,我們第一次觸發(fā)回調(diào)的時(shí)候才會開始計(jì)時(shí),如果最后一次觸發(fā)回調(diào)事件與前一次時(shí)間間隔小于delayTime葡粒,delayTime之后仍會執(zhí)行fn份殿。

這兩種方式有點(diǎn)優(yōu)勢互補(bǔ)的意思,哈哈~

我們考慮把這兩種方式結(jié)合起來嗽交,便會在第一次觸發(fā)事件時(shí)執(zhí)行fn卿嘲,最后一次與前一次間隔比較短,delayTime之后再次執(zhí)行fn夫壁。

想法簡單實(shí)現(xiàn)如下:

var throttle = function(fn, delayTime) {  
var flag, _start = Date.now();  
return function() {    
var context = this,      
    args = arguments,      
    _now = Date.now(),     
    remainTime = delayTime - (_now - _start);    
    if(remainTime <= 0) {
      fn.apply(this, args);
     } else {
      setTimeout(function () {
        fn.apply(this, args);
      }, remainTime)
  }    
}

通過上面的分析拾枣,可以很明顯的看出函數(shù)防抖和函數(shù)節(jié)流的區(qū)別:

頻繁觸發(fā)事件時(shí),函數(shù)防抖只會在最后一次觸發(fā)事件只會才會執(zhí)行回調(diào)內(nèi)容盒让,其他情況下會重新計(jì)算延遲事件梅肤,而函數(shù)節(jié)流便會很有規(guī)律的每隔一定時(shí)間執(zhí)行一次回調(diào)函數(shù)。

requestAnimationFrame
之前邑茄,我們使用setTimeout簡單實(shí)現(xiàn)了防抖和節(jié)流功能姨蝴,如果我們不考慮兼容性,追求精度比較高的頁面效果肺缕,可以考慮試試html5提供的API--requestAnimationFrame左医。

與setTimeout相比,requestAnimationFrame的時(shí)間間隔是由系統(tǒng)來決定同木,保證屏幕刷新一次浮梢,回調(diào)函數(shù)只會執(zhí)行一次,比如屏幕的刷新頻率是60HZ彤路,即間隔1000ms/60會執(zhí)行一次回調(diào)黔寇。

var throttle = function(fn, delayTime) {  
var flag;  
return function() {    
if(!flag) {
      requestAnimationFrame(function() {
        fn();
        flag = false;
      });
      flag = true;
    }
  }
}  

上述代碼的基本功能就是保證在屏幕刷新的時(shí)候(對于大多數(shù)的屏幕來說,大約16.67ms)斩萌,可以執(zhí)行一次回調(diào)函數(shù)fn缝裤。使用這種方式也存在一種比較明顯的缺點(diǎn)屏轰,時(shí)間間隔只能跟隨系統(tǒng)變化,我們無法修改憋飞,但是準(zhǔn)確性會比setTimeout高一些霎苗。

注意:

  • 防抖和節(jié)流只是減少了事件回調(diào)函數(shù)的執(zhí)行次數(shù),并不會減少事件的觸發(fā)頻率榛做。
  • 防抖和節(jié)流并沒有從本質(zhì)上解決性能問題唁盏,我們還應(yīng)該注意優(yōu)化我們事件回調(diào)函數(shù)的邏輯功能,避免在回調(diào)中執(zhí)行比較復(fù)雜的DOM操作检眯,減少瀏覽器reflow和repaint厘擂。
    上面的示例代碼比較簡單,只是說明了基本的思路锰瘸。目前已經(jīng)有工具庫實(shí)現(xiàn)了這些功能刽严,比如underscore,考慮的情況也會比較多避凝,大家可以去查看源碼舞萄,學(xué)習(xí)作者的思路,加深理解管削。

underscore的debounce方法源碼:

_.debounce = function(func, wait, immediate) {    
var timeout, result;    
var later = function(context, args) {
    timeout = null;      
    if (args) result = func.apply(context, args);
};    
var debounced = restArguments(function(args) {      
    if (timeout) clearTimeout(timeout);      
    if (immediate) {        
        var callNow = !timeout;
        timeout = setTimeout(later, wait);        
        if (callNow) result = func.apply(this, args);
    } else {
        timeout = _.delay(later, wait, this, args);
    }
        return result;
});

debounced.cancel = function() {
    clearTimeout(timeout);
    timeout = null;
};

return debounced;
};

underscore的throttle源碼:

_.throttle = function(func, wait, options) {    
    var timeout, context, args, result;    
    var previous = 0;    
    if (!options) options = {};    
    var later = function() {
        previous = options.leading === false ? 0 : _.now();
        timeout = null;
        result = func.apply(context, args);      
        if (!timeout) context = args = null;
    };
    
    var throttled = function() {      
        var now = _.now();      
        if (!previous && options.leading === false) previous = now;      
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;      
        if (remaining <= 0 || remaining > wait) {        
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
        previous = now;
        result = func.apply(context, args);        
        if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
      }
        return result;
    };

    throttled.cancel = function() {
      clearTimeout(timeout);
      previous = 0;
      timeout = context = args = null;
    };
    return throttled;
};
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末倒脓,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子含思,更是在濱河造成了極大的恐慌崎弃,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件含潘,死亡現(xiàn)場離奇詭異饲做,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)调鬓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酌伊,“玉大人腾窝,你說我怎么就攤上這事【幼” “怎么了虹脯?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長奏候。 經(jīng)常有香客問我循集,道長,這世上最難降的妖魔是什么蔗草? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任咒彤,我火速辦了婚禮疆柔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘镶柱。我一直安慰自己旷档,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布歇拆。 她就那樣靜靜地躺著鞋屈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪故觅。 梳的紋絲不亂的頭發(fā)上厂庇,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機(jī)與錄音输吏,去河邊找鬼权旷。 笑死,一個(gè)胖子當(dāng)著我的面吹牛评也,可吹牛的內(nèi)容都是我干的炼杖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼盗迟,長吁一口氣:“原來是場噩夢啊……” “哼坤邪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起罚缕,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤艇纺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后邮弹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體黔衡,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年腌乡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盟劫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡与纽,死狀恐怖侣签,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情急迂,我是刑警寧澤影所,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站僚碎,受9級特大地震影響猴娩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一卷中、第九天 我趴在偏房一處隱蔽的房頂上張望矛双。 院中可真熱鬧,春花似錦仓坞、人聲如沸背零。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽徙瓶。三九已至,卻和暖如春嫉称,著一層夾襖步出監(jiān)牢的瞬間侦镇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工织阅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留壳繁,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓荔棉,卻偏偏與公主長得像闹炉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子润樱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容