性能優化之防抖函數---debounce

在頁面上的某些事件觸發頻率很是高,好比滾動條滾動、窗口尺寸變化、鼠標移動等,若是咱們須要註冊這類事件,不得不考慮效率問題。而防抖函數就是爲了解決這一類問題而出現的。javascript

前言

在頁面上的某些事件觸發頻率很是高,好比滾動條滾動、窗口尺寸變化、鼠標移動等,若是咱們須要註冊這類事件,不得不考慮效率問題。html

當窗口尺寸發生變化時,哪怕只變化了一點點,都有可能形成成百上千次對處理函數的調用,這對網頁性能的影響是極其巨大的。java

因而,咱們能夠考慮,每次窗口尺寸變化、滾動條滾動、鼠標移動時,不要當即執行相關操做,而是等一段時間,以窗口尺寸中止變化、滾動條再也不滾動、鼠標再也不移動爲計時起點,一段時間後再去執行操做。git

例子

咱們來列舉一個關於鼠標移動的例子:github

<div id="container"></div>
複製代碼
div{
    height: 200px;
    line-height: 200px;
    text-align: center; color: #fff;
    background-color: #444;
    font-size: 25px;
    border-radius: 3px;
}
複製代碼
let count = 1;
let container = document.getElementsByTagName('div')[0];
function updateCount() {
    container.innerHTML = count ++ ;
}
container.addEventListener('mousemove',updateCount);
複製代碼

咱們來看一下效果:瀏覽器

avatar

咱們能夠看到,鼠標從左側滑到右側,咱們綁定的事件執行了119屢次緩存

這個例子很簡單,瀏覽器徹底反應的過來,但若是在頻繁的事件回調中作複雜計算,頗有可能致使頁面卡頓,不如將屢次計算合併爲一次計算,只在一個精確點作操做。app

爲了處理這個問題,通常有兩種解決方案:函數

  • debounce 防抖
  • throttle 節流

PS:防抖函數和節流節流函數的做用都是防止函數屢次調用。區別在於,假設一個用戶一直觸發這個函數,咱們設定一個最小觸發時間,當每次觸發函數的間隔小於最小觸發時間,防抖的狀況下只會調用一次,而節流的 狀況會每隔一個最小觸發時間調用函數。性能

關於節流函數部分,請看下一篇文章。

防抖

防抖的原理就是:你儘管觸發事件,可是我必定在事件觸發 n 秒後才執行,若是你在一個事件觸發的 n 秒內又觸發了這個事件,那我就以新的事件的時間爲準,n 秒後才執行,總之,就是要等你觸發完事件 n 秒內再也不觸發事件,我才執行,真是任性吶!

防抖的簡單實現

/**
 * 防抖函數
 * @param func 用戶傳入的防抖函數
 * @param wait 等待的時間
 */
const debounce = function (func,wait = 50) {
    // 緩存一個定時器id
    let timer = null;
    // 這裏返回的函數時每次用戶實際調用的防抖函數
    // 若是已經設定過定時器了就清空上一次的定時器
    // 開始一個定時器,延遲執行用戶傳入的方法
    return function(...args){
        if(timer) clearTimeout(timer);
        timer = setTimeout(()=>{
            //將實際的this和參數傳入用戶實際調用的函數
            func.apply(this,args);
        },wait);
    }
};
複製代碼

使用這個防抖函數應用在最開始的例子上:

container.addEventListener('mousemove',debounce(updateCount,100));
複製代碼

avatar

咱們能夠看到,無論咱們怎麼移動,咱們綁定的回調事件都是在鼠標中止後100ms後纔會觸發。

這是一個簡單版的防抖,可是有缺陷,這個防抖只能在最後調用。通常的防抖會有immediate選項,表示是否當即調用。這二者的區別,舉個栗子來講:

  • 在搜索引擎搜索問題的時候,咱們固然是但願用戶輸入完最後一個字才調用查詢接口,這個時候適用延遲執行的防抖函數,它老是在一連串(間隔小於wait的)函數觸發以後調用。
  • 用戶在點讚的時候,咱們但願用戶點第一下的時候就去調用接口,而且成功以後改變star按鈕的樣子,用戶就能夠立馬獲得反饋是否star成功了,這個狀況適用當即執行的防抖函數,它老是在第一次調用,而且下一次調用必須與前一次調用的時間間隔大於wait纔會觸發。

當即執行的防抖函數

/**
 * 防抖函數
 * @param func 用戶傳入的防抖函數
 * @param wait 等待的時間
 * @param immediate 是否當即執行
 */
const debounce = function (func,wait = 50,immediate = false) {
    // 緩存一個定時器id
    let timer = null;
    // 這裏返回的函數時每次用戶實際調用的防抖函數
    return function(...args){
        // 若是已經設定過定時器了就清空上一次的定時器
        if(timer) clearTimeout(timer);
        if(immediate){
            let callNow = !timer;
            //等待wait的時間間隔後,timer爲null的時候,函數才能夠繼續執行
            timer = setTimeout(()=>{
                timer = null;
            },wait);
            //未執行過,執行
            if(callNow) func.apply(this,args);
        }else{
            // 開始一個定時器,延遲執行用戶傳入的方法
            timer = setTimeout(()=>{
                //將實際的this和參數傳入用戶實際調用的函數
                func.apply(this,args);
            },wait);
        }
    }
};
複製代碼

avatar

返回值

此時要注意,用戶傳入的函數多是有返回值的,可是當immediate爲false的時候,由於使用了setTimeout,函數的返回值永遠爲undefined,因此咱們只在immediate爲true的時候返回函數的返回值

/**
 * 防抖函數
 * @param func 用戶傳入的防抖函數
 * @param wait 等待的時間
 * @param immediate 是否當即執行
 */
const debounce = function (func,wait = 50,immediate = false) {
    // 緩存一個定時器id
    let timer = null;
    let result;
    // 這裏返回的函數時每次用戶實際調用的防抖函數
    return function(...args){
        // 若是已經設定過定時器了就清空上一次的定時器
        if(timer) clearTimeout(timer);
        if(immediate){
            let callNow = !timer;
            //等待wait的時間間隔後,timer爲null的時候,函數才能夠繼續執行
            timer = setTimeout(()=>{
                timer = null;
            },wait);
            //未執行過,執行
            if(callNow) result = func.apply(this,args);
        }else{
            // 開始一個定時器,延遲執行用戶傳入的方法
            timer = setTimeout(()=>{
                //將實際的this和參數傳入用戶實際調用的函數
                func.apply(this,args);
            },wait);
        }
        return result;
    }
};
複製代碼

取消

最後咱們再思考一個小需求,我但願能取消 debounce 函數,好比說我 debounce 的時間間隔是 10 秒鐘,immediate 爲 true,這樣的話,我只有等 10 秒後才能從新觸發事件,如今我但願有一個按鈕,點擊後,取消防抖,這樣我再去觸發,就能夠又馬上執行啦

/**
 * 防抖函數
 * @param func 用戶傳入的防抖函數
 * @param wait 等待的時間
 * @param immediate 是否當即執行
 */
const debounce = function (func,wait = 50,immediate = false) {
    // 緩存一個定時器id
    let timer = null;
    let result;
    let debounced = function (...args) {
        // 若是已經設定過定時器了就清空上一次的定時器
        if(timer) clearTimeout(timer);
        if(immediate){
            let callNow = !timer;
            //等待wait的時間間隔後,timer爲null的時候,函數才能夠繼續執行
            timer = setTimeout(()=>{
                timer = null;
            },wait);
            //未執行過,執行
            if(callNow) result = func.apply(this,args);
        }else{
            // 開始一個定時器,延遲執行用戶傳入的方法
            timer = setTimeout(()=>{
                //將實際的this和參數傳入用戶實際調用的函數
                func.apply(this,args);
            },wait);
        }
        return result;
    };
    debounced.cancel = function(){
        clearTimeout(timer);
        timer = null;
    };
    // 這裏返回的函數時每次用戶實際調用的防抖函數
    return debounced;
};
複製代碼

在原頁面的基礎上,修改以下

div{
    height: 200px;
    line-height: 200px;
    text-align: center; color: #fff;
    background-color: #444;
    font-size: 25px;
    border-radius: 3px;
}
複製代碼
<div id="container"></div>
<button id="cancel">點擊取消防抖</button>
複製代碼
let count = 1;
let container = document.getElementsByTagName('div')[0];
let button = document.getElementById('cancel');
function updateCount() {
    container.innerHTML = count ++ ;
}
let action = debounce(updateCount,10000,true);

container.addEventListener('mousemove',action);
button.addEventListener('click',action.cancel);
複製代碼

avatar

至此咱們已經完成了一個 debounce 函數

感謝我豐哥大力指導,更多學習內容請點擊它的GitHub

相關文章
相關標籤/搜索