1. 程式人生 > 其它 >wait函式_函式防抖:讓你的函式休息一下

wait函式_函式防抖:讓你的函式休息一下

技術標籤:wait函式回撥函式改變全域性變數

b358bae6358e99ca25cc67e10f092ce4.png

函式防抖,是指防止函式在極短的時間內反覆呼叫,造成資源的浪費

考慮一下電梯關門的場景,現代的大部分電梯都可以通過紅外,感知到是否有人進入,為了避免夾到人,同時為了等待後面的人,電梯關門的時間往往有這麼一種規則:始終保證電梯門在最後一個人進入後3秒後關閉。如果有人進入後,還沒有等到3秒又有人進來了,電梯門會以最後一次進入的時間為計時起點,重新等待3秒。

再考慮一個頁面上的場景,頁面上的某些事件觸發頻率非常高,比如滾動條滾動、視窗尺寸變化、滑鼠移動等,如果我們需要註冊這類事件,不得不考慮效率問題,又特別是事件處理中涉及到了大量的操作,比如:

window.onresize = function(){    //大量的dom操作}

當視窗尺寸發生變化時,哪怕只變化了一點點,都有可能造成成百上千次對處理函式的呼叫,這對網頁效能的影響是極其巨大的。於是,我們可以考慮,每次視窗尺寸變化、滾動條滾動、滑鼠移動時,不要立即執行相關操作,而是等一段時間,以視窗尺寸停止變化、滾動條不再滾動、滑鼠不再移動為計時起點,一段時間後再去執行操作,就像電梯關門那樣。

再考慮一個搜尋的場景(例如百度),當我在一個文字框中輸入文字(鍵盤按下事件)時,需要將文字傳送到伺服器,並從伺服器得到搜尋結果,這樣的話,使用者直接輸入搜尋文字就可以了,不用再去點搜尋按鈕,可以提升使用者體驗,類似於下面的效果:

上面的效果,我沒有點選搜尋按鈕,也沒有按回車鍵,只是寫了一些搜尋的文字而已。

可是如何來實現上面的場景呢?如果文字框的文字每次被改變(鍵盤按下事件),我都要把資料傳送到伺服器,得到搜尋結果,這是非常恐怖的!想想看,我搜索“google”這樣的單詞,至少需要按6次按鍵,就這一個詞,我需要向伺服器請求6次,並讓伺服器去搜索6次,但我只需要最後一次的結果就可以了。如果考慮使用者按錯的情況,傳送請求的次數更加恐怖。這樣就造成了大量的頻寬被佔用,浪費了很多資源。

如何避免這樣的問題呢?你再仔細看看百度的處理方式:

你會發現,真正的搜尋行為,並不是每次按鍵都會觸發的,只有當用戶停止按鍵一段事件後才會觸發。

於是,為了滿足這種型別場景,我們可以開發一個通用的函式,這個函式要滿足以下功能:

  1. 呼叫該函式後,不立即做事,而是一段時間後去做事
  2. 如果在等待時間內呼叫了該函式,重新計時

這樣的功能,就叫做函式防抖,其實就是防止函式短時間內被呼叫多次。要完成該函式,需要給予兩個條件:

  1. 告訴我一段時間後要做什麼事(這裡應該是一個回撥函式,即函式作為引數)
  2. 告訴我要等待多長時間(毫秒)

基於這樣的理解,我們可以編寫如下的程式碼:

//設定全域性變數,記錄setTimeout得到的idlet timerId = null;/**
 * 函式防抖
 * @param {function} func 一段時間後,要呼叫的函式
 * @param {number} wait 等待的時間,單位毫秒
 */function debounce(func, wait){    if(timerId){        //如果有值,說明目前正在等待中,清除它
        clearTimeout(timerId);
    }    //重新開始計時
    timerId = setTimeout(() => {
        func();
    }, wait);
}

現在來測試一下這個函式:

<input type="text" id="txt"><script>
    document.getElementById("txt").onkeyup = (event)=>{        //500毫秒內沒有再次呼叫debounce,才會真正執行回撥函式
        debounce(()=>{            console.log(event.target.value);
        }, 500);
    }</script>

效果如下:

雖然我們實現了效果,可是仔細想想,目前這個防抖函式還是太年輕,太簡單,有時候還很幼稚。

因為它使用了一個全域性變數timerId,這意味著,頁面上所有需要防抖的地方都要共用這個變數,如果我同時要對視窗尺寸改變事件和文字框按鍵事件進行防抖,就辦不到了。

於是,我決定把這個變數寫到函式裡面去:

/**
 * 函式防抖
 * @param {function} func 一段時間後,要呼叫的函式
 * @param {number} wait 等待的時間,單位毫秒
 */function debounce(func, wait){    //設定變數,記錄setTimeout得到的id
    let timerId = null;    if(timerId){        //如果有值,說明目前正在等待中,清除它
        clearTimeout(timerId);
    }    //重新開始計時
    timerId = setTimeout(() => {
        func();
    }, wait);
}

但是這樣子還是有問題,這樣的話,每次呼叫防抖函式都會得到一個新的計時器,過去的計時器找不到了。

為了解決這個問題,我決定把程式碼改造成以下結構:

/**
 * 函式防抖
 * @param {function} func 一段時間後,要呼叫的函式
 * @param {number} wait 等待的時間,單位毫秒
 */function debounce(func, wait){    //設定變數,記錄setTimeout得到的id
    let timerId = null;    return function(){        if(timerId){            //如果有值,說明目前正在等待中,清除它
            clearTimeout(timerId);
        }        //重新開始計時
        timerId = setTimeout(() => {
            func();
        }, wait);
    }
}

這樣一來,debounce函式僅僅負責生成一個timerId,保證每次呼叫debounce都會有一個新的timerId產生即可,將防抖的工作,交給了返回的函式去完成。今後如果我要對某一個操作進行防抖,我只需要呼叫debounce函式來得到一個操作函式,後面反覆去呼叫操作函式即可。

下面是測試的程式碼:

let txtChange = debounce(() => {    console.log(document.getElementById("txt").value);
}, 500);document.getElementById("txt").onkeyup = () => {
    txtChange();
}let winChange = debounce(() => {    console.log("視窗尺寸改變了");
}, 500);window.onresize = () => {
    winChange();
}

下面是執行效果:

目前,我們的防抖函式還無法傳遞引數給回撥函式,只要稍稍做一點改變,即可實現引數的傳遞:

/**
 * 函式防抖
 * @param {function} func 一段時間後,要呼叫的函式
 * @param {number} wait 等待的時間,單位毫秒
 */function debounce(func, wait){    //設定變數,記錄setTimeout得到的id
    let timerId = null;    return function(...args){        if(timerId){            //如果有值,說明目前正在等待中,清除它
            clearTimeout(timerId);
        }        //重新開始計時
        timerId = setTimeout(() => {
            func(...args);
        }, wait);
    }
}

之後,我們就可以在呼叫時傳遞引數了:

let txtChange = debounce((event) => {    console.log(event.target.value);
}, 500);document.getElementById("txt").onkeyup = (event) => {
    txtChange(event); //將引數傳遞給回撥函式}

有了防抖函式,我們將來遇到滑鼠移動、視窗尺寸改變等需要大量呼叫同一個函式的時候,就可以使用它來降低執行頻率,保證執行效率。