性能优化之防抖函数---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

相关文章
相关标签/搜索