終于搞懂:防抖和節(jié)流

一鞋仍、緩沖

緩沖家妆,這是一個(gè)非常普遍的概念珊豹,凡是使某種事物進(jìn)行減慢或減弱變化過(guò)程都可以叫緩沖簸呈。網(wǎng)頁(yè)交互中也需要緩沖,也就是 防抖(debounce)節(jié)流(throttle) —— 讓高頻事件的反應(yīng)慢一點(diǎn)店茶,雖然是老生常談蜕便,但也不容忽視。

Lodash 中已早有實(shí)現(xiàn)忽妒。

  • debounce:一個(gè)事件被觸發(fā)后玩裙,立即設(shè)定一個(gè)小延時(shí)。如果在延時(shí)期間事件再次被觸發(fā)段直,則銷(xiāo)毀前一個(gè)延時(shí)并重新開(kāi)始延時(shí)吃溅;否則,開(kāi)始執(zhí)行處理函數(shù)鸯檬。簡(jiǎn)而言之决侈,就是不斷地重新設(shè)置計(jì)時(shí)器舉例:電梯門(mén)喧务,若干秒內(nèi)無(wú)人進(jìn)入赖歌,則自動(dòng)關(guān)門(mén)。
  • throttle:在事件連續(xù)觸發(fā)的過(guò)程中功茴,每隔一段時(shí)間執(zhí)行一次處理函數(shù)庐冯。舉例:文章自動(dòng)保存,在編輯過(guò)程中每隔幾分鐘自動(dòng)保存一次坎穿。

二展父、Debounce

1. debounce基本實(shí)現(xiàn)

  • 需求:鼠標(biāo)在頁(yè)面上移動(dòng),當(dāng)移動(dòng)停止 2 秒后玲昧,提示“鼠標(biāo)移動(dòng)了”栖茉。
  • 實(shí)現(xiàn):根據(jù)前面的定義,在鼠標(biāo)移動(dòng)時(shí)設(shè)置新的計(jì)時(shí)器即可:
let timer = null;
function setTimer(){
    timer && clearTimeout(timer);
    timer = setTimeout(function () {
        console.log("鼠標(biāo)移動(dòng)了");
    }, 2000)
}
window.onmousemove = setTimer;

2. 封裝

  • 需求:由于以上代碼使用頻繁孵延,需要封裝成一個(gè)便捷的工具函數(shù)吕漂,參數(shù)為事件處理函數(shù)和等待時(shí)長(zhǎng)。
  • 實(shí)現(xiàn):實(shí)參不一定會(huì)嚴(yán)格按照形參來(lái)傳尘应,注意防御
/**
 * debounce函數(shù) V1.0
 * @param {function} fn 事件處理函數(shù)惶凝,默認(rèn)為一個(gè)空函數(shù)
 * @param {number} delay 延時(shí)時(shí)間,單位為毫秒
 * @return {function} resetTimer 返回一個(gè)可以重新設(shè)定計(jì)時(shí)器的函數(shù)
 */
function debounce(fn=function(){}, delay=0){
    let timer = null;
    function resetTimer() {
        if(timer) {clearTimeout(timer)}
        // 要保證參數(shù)delay是自然數(shù)
        const delayTime = isNaN(parseInt(delay)) ? 
            0 : Math.abs(parseInt(delay));
        timer = setTimeout(fn, delayTime);
    }
    return resetTimer;
}

function onMove() { console.log("鼠標(biāo)移動(dòng)了") }
window.onmousemove = debounce(onMove, 2000);

3. 執(zhí)行環(huán)境:this

在事件處理函數(shù)中經(jīng)常需要訪問(wèn)this對(duì)象犬钢。 上面的debounce函數(shù)被用在了window.onmousemove上梨睁,所以onMove里的this肯定指的是window。現(xiàn)在我們把debounce用在別的對(duì)象上娜饵,看看this的指向會(huì)不會(huì)變化:

// ...debounce函數(shù) V1.0
//假設(shè)有個(gè)ID為menu的元素
const oBtn = document.getElementById("menu"); 
function onBtnHover() {console.log(this)};
oBtn.onmousemove = debounce(onBtnHover, 1000);
// 結(jié)果:window對(duì)象

出問(wèn)題了,onBtnHover里的 this 并不是oBtn官辈,問(wèn)題在debounce函數(shù)的setTimeout里:

setTimeout(fn, delayTime); //相當(dāng)于
function callback(){
    fn();
}
setTimeout(callback, delayTime);

這里的 fn 并沒(méi)有與任何對(duì)象綁定箱舞,所以它屬于window對(duì)象遍坟,因此this指向window。

4. 解決this指向問(wèn)題

ES5: 一個(gè)函數(shù)在被調(diào)用時(shí)晴股,會(huì)自動(dòng)取得兩個(gè)特殊變量:this和arguments愿伴。在全局環(huán)境調(diào)用函數(shù)時(shí),this代表window對(duì)象电湘;而當(dāng)函數(shù)被作為其他某個(gè)對(duì)象的方法而調(diào)用時(shí)隔节,this就代表那個(gè)對(duì)象。

簡(jiǎn)而言之: ES5的函數(shù)中寂呛,this是執(zhí)行時(shí)函數(shù)所在的那個(gè)對(duì)象怎诫。

ES6: this是函數(shù)定義所在的那個(gè)對(duì)象。

在下面的代碼中贷痪,debounce為oBtn.onmousemove綁定了事件處理函數(shù)resetTimer幻妓。根據(jù)ES5中this的性質(zhì),該函數(shù)運(yùn)行時(shí)劫拢,它的執(zhí)行環(huán)境必然是調(diào)用者:

function debounce(fn=function(){}, delay=0){
    function resetTimer() {
       console.log("resetTimer執(zhí)行環(huán)境:", this);
    }
    return resetTimer;
}
const oBtn = document.getElementById("menu");
function onBtnHover() {console.log(this)};
oBtn.onmousemove = debounce(onBtnHover, 1000);
// 結(jié)果:執(zhí)行環(huán)境是oBtn

因此肉津,應(yīng)該在resetTimer函數(shù)里將 this 保存下來(lái),然后在setTimeout中使用call舱沧、apply函數(shù)修改this指向妹沙。debounce函數(shù)可以這樣修改:

/**
 * debounce函數(shù) V2.0
 * @param {function} fn 事件處理函數(shù),默認(rèn)為一個(gè)空函數(shù)
 * @param {number} delay 延時(shí)時(shí)間熟吏,單位為毫秒
 * @return {function} resetTimer 返回一個(gè)可以重新設(shè)定計(jì)時(shí)器的函數(shù)
 */
function debounce(fn=function(){}, delay=0){
    let timer = null;
    function resetTimer() {
        const context = this;  /**** 修改:保存執(zhí)行環(huán)境 ****/ 
        if(timer) {clearTimeout(timer)}
        const delayTime = isNaN(parseInt(delay)) ? 
            0 : Math.abs(parseInt(delay));
        timer = setTimeout(
            function(){fn.apply(context)},  /**** 修改:應(yīng)用執(zhí)行環(huán)境 ****/ 
            delayTime
        );
    }
    return resetTimer;
}

const oBtn = document.getElementById("menu");
function onBtnHover() {console.log("鼠標(biāo)移動(dòng)了距糖,執(zhí)行環(huán)境是:", this)};
oBtn.onmousemove = debounce(onBtnHover, 1000);
// 結(jié)果:執(zhí)行環(huán)境是 oBtn

5. 參數(shù)傳遞

上面的代碼保證了this指向正確,但是一旦試圖為onBtnHover傳參分俯,就會(huì)影響到this指向肾筐。

  • 需求:onBtnHover函數(shù)打印的是為其傳遞的字符串
  • 實(shí)現(xiàn):用一個(gè)匿名函數(shù)調(diào)用onBtnHover并傳參
// ...debounce函數(shù) V2.0
const oBtn = document.getElementById("menu");
function onBtnHover(text) {console.log(text, this)};
oBtn.onmousemove = debounce(
    function(){onBtnHover("鼠標(biāo)移動(dòng)了,執(zhí)行環(huán)境是:")}, 
    1000
);
// 結(jié)果:執(zhí)行環(huán)境是 window
  • 原因: 為了傳參缸剪,我們用一個(gè)匿名函數(shù)來(lái)調(diào)用onBtnHover吗铐,因此apply(context)只是修改了這個(gè)匿名函數(shù)的this,而onBtnHover的this仍然未被修改杏节。
  • 證明:在這個(gè)匿名函數(shù)內(nèi)打印一下this
// ...debounce函數(shù) V2.0
const oBtn = document.getElementById("menu");
function onBtnHover(text) {console.log(text, this)};
oBtn.onmousemove = debounce(
    function(){
        console.log("匿名函數(shù)執(zhí)行環(huán)境:", this); /*** 修改 ***/
        onBtnHover("鼠標(biāo)移動(dòng)了唬渗,執(zhí)行環(huán)境是:");
    }, 
    1000
);
// 匿名函數(shù)執(zhí)行環(huán)境:oBtn
// 鼠標(biāo)移動(dòng)了,執(zhí)行環(huán)境是:window
  • 解決方法1: 在匿名函數(shù)里使用call奋渔、apply修改onBtnHover的this镊逝,并傳參
// ...debounce函數(shù) V2.0
const oBtn = document.getElementById("menu");
function onBtnHover(text) {console.log(text, this)};
oBtn.onmousemove = debounce(
    function(){
        console.log("匿名函數(shù)執(zhí)行環(huán)境:", this); 
        onBtnHover.call(this, "鼠標(biāo)移動(dòng)了,執(zhí)行環(huán)境是:"); /*** 修改 ***/
    }, 
    1000
);
// 匿名函數(shù)執(zhí)行環(huán)境:oBtn
// 鼠標(biāo)移動(dòng)了嫉鲸,執(zhí)行環(huán)境是:oBtn

注意:以上代碼中如果使用apply撑蒜,則參數(shù)必須以數(shù)組的形式傳遞:onBtnHover.apply(this, ["鼠標(biāo)移動(dòng)了,執(zhí)行環(huán)境是:"])

call()方法與 apply()方法的作用相同,它們的區(qū)別僅在于接收參數(shù)的方式不同座菠。前者需要將參數(shù)列舉出來(lái)狸眼,后者則需要將參數(shù)放到數(shù)組里。

  • 解決方法2:直接將onBtnHover定義為oBtn的一個(gè)方法
// ...debounce函數(shù) V2.0
const oBtn = document.getElementById("menu");
oBtn.onBtnHover = function(text) {console.log(text, this)};  /*** 修改 ***/
oBtn.onmousemove = debounce(
    function(){
        console.log("匿名函數(shù)執(zhí)行環(huán)境:", this);
        oBtn.onBtnHover("鼠標(biāo)移動(dòng)了浴滴,執(zhí)行環(huán)境是:"); /*** 修改 ***/
    }, 
    1000
);
// 匿名函數(shù)執(zhí)行環(huán)境:oBtn指向的元素
// 鼠標(biāo)移動(dòng)了拓萌,執(zhí)行環(huán)境是:oBtn指向的元素

6. Debounce:總結(jié)

Debounce是在綁定事件處理函數(shù)時(shí)使用的,它會(huì)為事件綁定一個(gè)不斷重新設(shè)定計(jì)時(shí)器的函數(shù)升略。使用上面的debounce函數(shù) V2.0 時(shí)需要向其傳遞真正的事件處理函數(shù)以及延時(shí)時(shí)間微王。 如果需要給事件處理函數(shù)傳參,則要用上述的兩種方式來(lái)控制this指向品嚣。

三炕倘、Throttle

節(jié)流與防抖的差別僅僅是控制方式不同,所以我們可以借鑒debounce的實(shí)現(xiàn)思路腰根。

1. throttle基本實(shí)現(xiàn)

  • 需求:在鼠標(biāo)移動(dòng)的過(guò)程中激才,每隔1秒打印一次“鼠標(biāo)在移動(dòng)”
  • 實(shí)現(xiàn):鼠標(biāo)剛移動(dòng)就會(huì)設(shè)定一個(gè)計(jì)時(shí)器,同時(shí)設(shè)定一個(gè)標(biāo)記额嘿,表明在計(jì)時(shí)期間若鼠標(biāo)再次移動(dòng)瘸恼,則什么都不做,計(jì)時(shí)結(jié)束后執(zhí)行處理函數(shù)册养,同時(shí)清除標(biāo)記东帅。這樣一來(lái),當(dāng)再次移動(dòng)鼠標(biāo)時(shí)球拦,新的計(jì)時(shí)器和標(biāo)記就會(huì)被設(shè)定……
/**
 * throttle函數(shù) V1.0
 * @param {function} fn 事件處理函數(shù)靠闭,默認(rèn)為一個(gè)空函數(shù)
 * @param {number} delay 延時(shí)時(shí)間,單位為毫秒
 * @return {function} setNewTimer 返回一個(gè)可以按時(shí)間間隔設(shè)定計(jì)時(shí)器的函數(shù)
 */
function throttle(fn = function(){}, delay = 0){
    let isTiming = false; // 是否正在計(jì)時(shí)
    let timer = null;
    function setNewTimer(){
        if(isTiming) {return};

        //開(kāi)始設(shè)置計(jì)時(shí)器和標(biāo)記
        isTiming = true; // 設(shè)定標(biāo)記坎炼,阻止下次觸發(fā)
        const delayTime = isNaN(parseInt(delay)) ? 
            0 : Math.abs(parseInt(delay));
        let context = this;
        if(timer){clearTimeout(timer)}; //清除已有計(jì)時(shí)器
        timer = setTimeout(function(){
            fn.apply(context);
            isTiming = false;
        }, delayTime);
    }
    return setNewTimer;
}

function onMove(){console.log( "鼠標(biāo)在移動(dòng)" )};
window.onmousemove = throttle(onMove, 1000);

2. 傳參與this指向

  • 需求:鼠標(biāo)在指定的元素上移動(dòng)時(shí)每隔1秒打印一次“鼠標(biāo)在移動(dòng)”愧膀,并且保證this指向該元素。
  • 實(shí)現(xiàn):使用前面提到的兩種方式即可
  • 方案1:在匿名函數(shù)里使用call谣光、apply修改onBtnHover的this檩淋,并傳參
// throttle函數(shù) V1.0
const oBtn = document.getElementById("menu");
function onBtnHover(text){console.log( text, this )};
oBtn.onmousemove = throttle(
    function(){
        console.log("匿名函數(shù)執(zhí)行環(huán)境:", this);
        onBtnHover.call(this, "鼠標(biāo)在移動(dòng),執(zhí)行環(huán)境:")
}, 1000);
  • 方案2:直接將onBtnHover定義為oBtn的一個(gè)方法
// throttle函數(shù) V1.0
const oBtn = document.getElementById("menu");
oBtn.onBtnHover = function(text){console.log( text, this )};
oBtn.onmousemove = throttle(
    function(){
        console.log("匿名函數(shù)執(zhí)行環(huán)境:", this);
        oBtn.onBtnHover("鼠標(biāo)在移動(dòng)萄金,執(zhí)行環(huán)境:")
}, 1000);

四蟀悦、總結(jié)

至此,debounce和throttle均已實(shí)現(xiàn)完畢氧敢,我們還可以向它們添加更多參數(shù)來(lái)擴(kuò)展其功能日戈,比如用另外一個(gè)參數(shù)表示是否需要立即執(zhí)行,等等孙乖。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末浙炼,一起剝皮案震驚了整個(gè)濱河市份氧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鼓拧,老刑警劉巖半火,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異季俩,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)梅掠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門(mén)酌住,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人阎抒,你說(shuō)我怎么就攤上這事酪我。” “怎么了且叁?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵都哭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我逞带,道長(zhǎng)欺矫,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任展氓,我火速辦了婚禮穆趴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘遇汞。我一直安慰自己未妹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布空入。 她就那樣靜靜地躺著络它,像睡著了一般。 火紅的嫁衣襯著肌膚如雪歪赢。 梳的紋絲不亂的頭發(fā)上化戳,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音轨淌,去河邊找鬼迂烁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛递鹉,可吹牛的內(nèi)容都是我干的盟步。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼躏结,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼却盘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤黄橘,失蹤者是張志新(化名)和其女友劉穎兆览,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體塞关,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抬探,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了帆赢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片小压。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖椰于,靈堂內(nèi)的尸體忽然破棺而出怠益,到底是詐尸還是另有隱情,我是刑警寧澤瘾婿,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布蜻牢,位于F島的核電站,受9級(jí)特大地震影響偏陪,放射性物質(zhì)發(fā)生泄漏抢呆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一竹挡、第九天 我趴在偏房一處隱蔽的房頂上張望镀娶。 院中可真熱鬧,春花似錦揪罕、人聲如沸梯码。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)轩娶。三九已至,卻和暖如春框往,著一層夾襖步出監(jiān)牢的瞬間鳄抒,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工椰弊, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留许溅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓秉版,卻偏偏與公主長(zhǎng)得像贤重,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子清焕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

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

  • 有些情況下會(huì)產(chǎn)生問(wèn)題: 向后臺(tái)發(fā)送數(shù)據(jù)并蝗,用戶(hù)頻繁觸發(fā)祭犯,對(duì)服務(wù)器造成壓力 一些瀏覽器事件:window.onresi...
    bounsail閱讀 1,048評(píng)論 0 1
  • 防抖和節(jié)流都是為了解決短時(shí)間內(nèi)大量觸發(fā)某函數(shù)而導(dǎo)致的性能問(wèn)題,比如觸發(fā)頻率過(guò)高導(dǎo)致的響應(yīng)速度跟不上觸發(fā)頻率滚停,出現(xiàn)延...
    _SweetHeart閱讀 388評(píng)論 0 1
  • 防抖和節(jié)流的區(qū)別沃粗,防抖是把多次操作合并成一個(gè)觸發(fā)執(zhí)行,節(jié)流相當(dāng)于開(kāi)水龍頭键畴,水大了關(guān)小最盅,一段時(shí)間執(zhí)行一次 函數(shù)防抖(...
    coderXuan閱讀 387評(píng)論 0 1
  • Ps: 比如搜索框,用戶(hù)在輸入的時(shí)候使用change事件去調(diào)用搜索镰吵,如果用戶(hù)每一次輸入都去搜索的話檩禾,那得消耗多大的...
    燕自浩閱讀 5,964評(píng)論 0 1
  • 今天青石的票圈出鏡率最高的侨核,莫過(guò)于張藝謀的新片終于定檔了草穆。 一張滿(mǎn)溢著水墨風(fēng)的海報(bào)一次次的出現(xiàn)在票圈里,也就是老謀...
    青石電影閱讀 10,343評(píng)論 1 2