函数防抖与节流

函数防抖

一、为什么需要防抖

  • 高频的函数操作可能产生不好的影响
  • 如:resize、scroll、mousedown、mousemove、keyup、keydown……

为此,我们举个示例代码来了解事件如何频繁的触发:

我们写一个 index.html 文件:



    
    
    
    Document
    debounce
    


    

从左边滑到右边就触发了近100次getUserAction 函数!看如下Gif:

函数防抖与节流_第1张图片

因为这个例子很简单,所以浏览器完全反应的过来,但假设:

  • 它的触发频次极高,1分钟2000次,且涉及到大量的位置计算、DOM 操作等工作,
  • 存在接口请求,单个函数执行时间较长,但每个函数触发的间隔很近。

这种在一瞬间(短时间内)对浏览器或服务器造成了过多压力的交互就需要进行优化了,为了解决这个问题,一般有两种解决方案:

  • debounce 防抖
  • throttle 节流

他们的目的都是:降低一个函数的触发频率,以提高性能或避免资源浪费。

二、防抖的原理

今天重点讲讲防抖的实现。

防抖的原理就是:你尽管触发事件,但是我一定在事件触发n秒无操作后才执行。举个例子:

我们规定3s为防抖的标准,那么:

  1. 第一次要求执行事件 - 此时倒计时3s
  2. 倒计时2s
  3. 倒计时1s
  4. 0.5s时事件再次被触发 - 此时倒计时3s
  5. …3s内无事发生
  6. 执行事件,共用了5.5s

三、自己实现一个防抖

3.1 第一版

我们根据上一节提到的核心思想,实现第一版代码:

function debounce(func, wait) {
    var timer;
    return function () {
        clearTimeout(timer)
        timer = setTimeout(func, wait);
    }
}

如果我们要使用它,第一节的例子为例:

oDiv.onmousemove = debounce(getUserAction, 2000);

此时大家可以再次测试一下,事件持续发生时,只有在完全停止2s后,才会触发事件:

写到这里,作为针对部分高频事件的需求来说,已经结束了。我们来看看他的效果:

函数防抖与节流_第2张图片

3.2 第二版

大家都知道,dom节点在触发事件的时候,this指向它本身,本例中则指向oDiv,但是在本例中:我们看一下

var count = 1;
var oDiv = document.getElementById("oDiv");
function getUserAction() {
    oDiv.innerHTML = count++;
    console.log('this', this); // 此时输出 Window...
}
oDiv.onmousemove = debounce(getUserAction, 2000);
function debounce(func, wait) {
    var timer;
    return function () {
        clearTimeout(timer)
        timer = setTimeout(func, wait);
    }
}

毕竟经过了一层匿名函数的包裹,this已经指向了window,为了减少影响,我们尝试修正它

function debounce(func, wait) {
    var timer;
    return function () {
        var _this = this; // 记录当前this
        clearTimeout(timer)
        timer = setTimeout(function(){
            func.apply(_this); //将 func的this改为_this
        }, wait);
    }
}
3.3 第三版

解决的this指向问题,我们的函数仍然不够“完美”,JavaScript中,事件处理函数会提供event对象,我们简称为e。

// 使用了 debouce 函数
function getUserAction(e) {
    console.log(e); // undefined
    oDiv.innerHTML = count++;
};

为了保证它的原汁原味,我们再改第三版:

var count = 1;
var oDiv = document.getElementById("oDiv");
function getUserAction(e) {
    oDiv.innerHTML = count++;
    console.log('e', e); // MouseEvent
}
oDiv.onmousemove = debounce(getUserAction, 2000);
function debounce(func, wait) {
    var timer;
    return function () {
        var _this = this; // 记录当前this
        var arg = arguments; // 记录参数
        clearTimeout(timer)
        timer = setTimeout(function () {
            func.apply(_this, arg); //将 func的this改为_this
        }, wait);
    }
}

到此为止,我们在尽可能保留Dom事件原有能力的情况下,给函数加上了防抖效果,它可以解决大部分我们日常开发的防抖问题,但我们需要更“完美”

四、防抖进阶

4.1 立即执行

这个需求就是:

  • 立即执行
  • 保持n秒空白期
  • n秒空白期置后

想想这个需求也是很有道理的嘛,那我们加个immediate参数判断是否是立刻执行。

function debounce(func, wait, immediate) {
    var timer;
    return function () {
        var _this = this;
        var args = arguments;
        if (timer) clearTimeout(timer); // 常规流程,间隔内触发时清掉重置定时
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timer; // 1. callNow 初始值是 true, 同步立即执行;随后 timer 才开始执行
            timer = setTimeout(function(){
                timer = null; // wait 期间,timer 是一个 ID 数字,所以 callNow 为 false,func 在此期间永远不会执行
            }, wait) // wait 之后,timer 赋值 null,callNow 为 true,func 又开始立即执行。
            if (callNow) func.apply(_this, args)
        }
        else {
            timer = setTimeout(function(){
                func.apply(_this, args)
            }, wait);
        }
    }
}

再来看下此时他是什么效果:

函数防抖与节流_第3张图片

4.2 添加简单验证
function debounce(func, wait, immediate) {
    var timer;
    // 检查函数
    if (typeof func !== 'function') {
        throw new TypeError('Expected a function');
    }
    // 保证wait存在
    wait = +wait || 0;
    const debounced = function () {
        var _this = this;
        var args = arguments;
        if (timer) clearTimeout(timer); // 常规流程,间隔内触发时清掉重置定时
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timer; // 如果不存在定时器,则callNow为true
            timer = setTimeout(function () {
                timer = null; // 为了保证之后的时效性,手动添加timer
            }, wait)
            // 因为不存在timer,证明是首次执行,所以直接调用
            if (callNow) func.apply(_this, args)
        }
        else {
            timer = setTimeout(function () {
                func.apply(_this, args)
            }, wait);
        }
    }
    return debounced
}
4.3 添加取消事件方法

如果你希望能取消被防抖的事件,我们可以这样写:

function debounce(func, wait, immediate) {
    var timer;
    // 检查函数
    if (typeof func !== 'function') {
        throw new TypeError('Expected a function');
    }
    // 保证wait存在
    wait = +wait || 0;
    const debounced = function () {
        var _this = this;
        var args = arguments;
        if (timer) clearTimeout(timer); // 常规流程,间隔内触发时清掉重置定时
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timer; // 如果不存在定时器,则callNow为true
            timer = setTimeout(function () {
                timer = null; // 为了保证之后的时效性,手动添加timer
            }, wait)
            // 因为不存在timer,证明是首次执行,所以直接调用
            if (callNow) func.apply(_this, args)
        }
        else {
            timer = setTimeout(function () {
                func.apply(_this, args)
            }, wait);
        }
    }
    const cancel = function(){
        clearTimeout(timer);
        timer = null;
    }
    const pending = function(){
        return timer !== undefined;
    }
    debounced.cancel = cancel;
    debounced.pending = pending;
    return debounced
}

我们再来看看效果:

函数防抖与节流_第4张图片

函数节流

前言

我们还是以移动事件举例




    
    
    
    Document
    


    

它的效果是这样:

函数防抖与节流_第5张图片

一、核心和基本实现

节流的原理很简单:如果你持续触发某个事件,特定的时间间隔内,只执行一次。

关于节流的实现,有两种主流的实现方式:

  1. 时间戳思路
  2. 定时器思路
1.1 时间戳思路

顾名思义,通过两个时间戳来控制时间间隔,当触发事件的时候:

  1. 我们取出当前的时间戳 now
  2. 然后减去之前执行时的时间戳(首次值为 0 ) prev
  3. 如果大now - prev > wait,证明时间区间维护结束,执行指定事件,更新prev

根据这一思路,我们就可以实现第一版代码了:

oWrapper.onmousemove = throttle(moveAction, 1000);
function throttle(func, wait) {
    var _this, arg;
    var prev = 0; // 上一次触发的时间,第一次默认为0
    return function () {
        var now = Date.now(); // 触发时的时间
        _this = this;
        if (now - prev > wait) {
            func.apply(_this, arg); // 允许传入参数,并修正this
            prev = now; // 更新上一次触发的时间
        }
    }
}

来看看借助它,效果是什么样的:

函数防抖与节流_第6张图片

我们可以看到:

  1. 当鼠标移入的时候,事件立刻执行
  2. 每过 1s 会执行一次,且移动2.5s会执行2次,意味着动作停止后不会再执行
1.2 定时器思路

利用定时器来保证间隔时间内事件的触发次数

  1. 创建定时器timer,记录当前是否在周期内;
  2. 判断定时器是否存在,若存在则直接结束,否则执行事件;
  3. wait时间之后再次执行,并清掉定时器;

当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。

function throttle(func, wait) {
    var _this, arg;
    var timer; // 初始化
    return function () {
        _this = this; // 记录this
        arg = arguments; // 记录参数数组
        if (timer) return; // 时候未到
        timer = setTimeout(() => {
            func.apply(_this, arg); // 允许传入参数,并修正this
            timer = null;
        }, wait);
    }
}

来看看借助它,效果是什么样的:

函数防抖与节流_第7张图片

但是,我们可以看到:

  1. 当鼠标移入的时候,事件不会立刻执行;
  2. 鼠标定制后wait间隔后会执行一次
1.3 两种思路的区别
时间戳 定时器
“起点” 立即执行 n 秒后执行
“终点” 停止后不会执行 停止会再执行一次

二、节流进阶

结合两种思想完成一个可以立即执行,且停止触发后再执行一次的节流方法:

// 第三版
function throttle(func, wait) {
    var timeout, context, args, result;
    var previous = 0;
    var later = function() {
        previous = +new Date();
        timeout = null;
        func.apply(context, args)
    };
    var throttled = function() {
        var now = +new Date();
        //下次触发 func 剩余的时间
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
         // 如果没有剩余的时间了或者你改了系统时间
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
        } else if (!timeout) {
            timeout = setTimeout(later, remaining);
        }
    };
    return throttled;
}

效果演示如下:

函数防抖与节流_第8张图片

我在看代码的时候,我是反复打印数据才理解为什么会这样做,一起加油~

欢迎访问我的个人博客:https://www.gaoyuan.ink,一起共勉学习~

你可能感兴趣的:(javascript,vue)