JS - 防抖&节流

阅读:
https://juejin.cn/post/7016502001911463950
https://juejin.cn/post/6844903480239325191
https://juejin.cn/post/6844903481761857543
https://juejin.cn/post/7040633388625035272

1 防抖

防抖的原理
就是:(连续触发一个事件,间隔时间没有超过n秒,都只执行一次) 多次触发事件,在事件触发 n 秒后才执行,如果在一个事件触发的 n 秒内又触发了这个事件,那就以新的事件的时间为准,n 秒后才执行,总之,就是要等触发完事件 n 秒内不再触发事件,才执行。
【关注的是间隔时间内"最后一次"操作后的结果反馈】

防抖常见情景

  1. 监听滚动条滚动(window 的scroll)(例如监听滚动条滚动要执行一些业务操作);
  2. 监听浏览器窗口变化(window 的 resize)(例如在 echarts 的应用中,默认浏览器窗口大小改变 echarts 视图布局是不会做响应式改变的,那么就需要通过监听浏览器窗口大小改变然后去重置 echarts 实现布局的改变。);
  3. 表单输入的一些监听事件,例如 oninput 等(例如做表单输入校验时,连续的输入几个字可能会触发多次校验,防抖可以做到一段时间内连续输入多个文字只校验一次);
  4. 鼠标事件,例如mousedown、mousemove(例如拖拽等的监听等,出于准确性和及时性, 他们的监听响应十分细密,而当这种频繁在业务上可能不是必要的,那么也可以考虑使用防抖动技术);
  5. 键盘事件,例如keyup、keydown

△模板

// 函数防抖的实现 -- 无immediate版
function debounce(fn, wait) {
  let timer;

  return function() {
    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) clearTimeout(timer);

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, wait);
  };
}
/** 
* 1.apply改变this指向
* 2.args--传入的func的参数对象
* 3.result--传入的func的返回值--问题:immediate不为true拿不到result,result在setTimeout里面了。所以呢???咋拿到呀待定
* */
function debounce(func, wait, immediate) {

    let timeout;
    //let result;

    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            let callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
            // if (callNow) result = func.apply(context, args)
            // return result;
        }
        else {
            timeout = setTimeout(function(){
                result = func.apply(context, args)
            }, wait);
        }

        // return result; 
 //问题:immediate为false拿不到result,result在setTimeout里面了,异步赋值。会先执行return result。
    }
}
  • 疑问1: 下面2种写法,2个this的位置区别?
    答:
    第1种 – // 定时器setTimeout是Window定义的,setTimeout(function(){console.log(this)},wait)的this指向Window,∴需要使用apply改变this指向为context。
    第2种 – 使用了箭头函数(ES6箭头函数里this的指向就是上下文里对象this指向,偶尔没有上下文对象,this就指向window),和第1种的this指向的位置是一样滴

在这里插入图片描述

在这里插入图片描述

  • 疑问2:不是每次滚动都执行debounce函数吗,但是滚动多次只打印一次timeout

:页面滚动执行的函数是debounce函数里面return的函数

在这里插入图片描述

function debounce(func, wait, immediate) {

    var timeout, result;
    console.log(timeout,'timeout')

    return function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) result = func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                result = func.apply(context, args)
            }, wait);
        }

        return result;
    }
};
function handle(){
    console.log('handle')
};
// 滚动事件
window.addEventListener('scroll', debounce(handle, 500));
  • 疑问3: 为什么setTimeout里面执行的func函数的this指向改变为context位置的this呢, 比如说,context在这个位置就赋值this不行吗(蓝色标记)
    在这里插入图片描述

答: 外部的this的上下文指向的是debounce函数的上下文,闭包函数内部的this指向的上下文是实际函数执行的上下文,∴当然不能把context放到这个位置赋值this。
再看下这个 闭包和this指向问题

  • 疑问3.2:那为什么谷歌浏览器中实现的效果是一样的呢?为什么func要使用apply?不使用apply直接执行func也可以实现效果?


蓝色和红色位置this指向刚好一样而已,谷歌浏览器中测试因为蓝色标记位置此时的this指向Window;
可以放到Vue文件里面测试看看,蓝色位置是debounce函数执行上下文,指向Vue,红色位置默认指向Window。

谷歌浏览器中测试结果:
在这里插入图片描述
Vue文件中测试结果(这个测试例子很奇怪,体现不了apply的作用其实。。因为debounce函数在methods里面定义,context0是指向Vue实例;而context默认指向Window;这样子传入this.handle无论有无apply都无所谓,因为this.handle传入的时候是本来就是指向Vue实例,是可以实现防抖效果,无关apply的事。不过一般防抖函数是定义在其他文件,被引入进来的,而不是定义在组件methods里面,这个例子不具代表性,继续看看有代表性的例子叭~↓):
在这里插入图片描述

<template>
  <div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
    <div>test</div>
  </div>
</template>

<script>
export default {
  name: 'Test',
  methods: {
    debounce(func, wait, immediate) {
      var timeout, result;
      var context0 = this;
      console.log(context0, 'context0');

      return function () {
        var context = this;
        var args = arguments;

        console.log(context, 'context');
        console.log(context0 == context, 'context0=context');

        if (timeout) clearTimeout(timeout);
        if (immediate) {
          // 如果已经执行过,不再执行
          var callNow = !timeout;
          timeout = setTimeout(function () {
            timeout = null;
          }, wait);
          if (callNow) result = func.apply(context, args);
        } else {
          timeout = setTimeout(function () {
            result = func.apply(context, args);
          }, wait);
        }

        return result;
      };
    },

    handle() {
      console.log(this, 'handle--this');
    },
  },
  mounted() {
    // 滚动事件
    window.addEventListener('scroll', this.debounce(this.handle, 2000));
  },
};
</script>

  • 疑问3.3具有代表性的体现apply作用的例子:

    ① 新建一个js文件Test.js:
export function debounce(func, wait, immediate) {
    var timeout, result;
    var context0 = this;
    console.log(context0, 'context0');

    return function () {
        var context = this;
        var args = arguments;

        console.log(context, 'context');

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function () {
                timeout = null;
            }, wait);
            if (callNow) result = func.apply(context, args);
        } else {
            timeout = setTimeout(function () {
                result = func.apply(context, args);
                // func();
            }, wait);
        }

        return result;
    };
}

②新建一个Vue文件:Test.vue

<template>
  <div>
    <div @click="testClick">test</div>
  </div>
</template>

<script>
import { debounce } from './test';

export default {
  name: 'Test',
  data() {
    return {
      a: 1,
    };
  },
  methods: {
    handle() {
      console.log(this.a, 'handle--this');
    },

    testClick: debounce(function () {
      //如果不使用apply改变this指向,这里的this是undefined;
      //   使用apply改变this指向后,这里的this是指向Vue实例
      console.log(this, 'thisthis');
      this.handle();
    }, 2000),
  },
};
</script>

③测试结果:先输出context0->点击div,输出context,2s后输出handle-this结果
在这里插入图片描述
![在这里插入图片描述](https://img-blog.csdnimg.cn/d076c153db54485b936e56ead5afbf7e.png在这里插入图片描述
为什么还没点击就输出content0?
testClick绑定的是debounce执行的返回值

  • 疑问4:immediate版意义
    :immediate为true时,一点击按钮立即执行handle函数,wait时间内再次点击按钮不再执行handle,间隔时间大于wait会再次立即执行handle。

  • 疑问5:immediate版为什么需要提前定义callNow变量、把timeout=置前?这样把timeout置前不行吗?

 if (immediate) {
     if (!timeout) result = func.apply(context, args);
     timeout = setTimeout(function () {
         timeout = null;
     }, wait);
 } 

:我的理解是让时间计算更精准一点。
如果改成上面那样timeout置后,用户点击按钮立即执行func,当执行完func之后才开始计算wait时间,下一次的时间间隔时间实际上是:执行func的时间+wait时间。
把timeout置前,间隔时间是:开始执行func到下次开始执行func时间间隔,这个时间更准确。

2 节流

节流的原理
很简单:如果你持续触发事件,每隔一段时间,只执行一次事件。
【关注的是操作过程中每隔一段时间的(持续的)反馈】

节流常见情景

  • 鼠标不断点击触发,mousedown(单位时间内只触发一次)
  • 监听滚动事件,比如判断是否滑到底部自动加载更多,用throttle来判断(单位时间内只判断一次)

△模板代码

function throttle(func, wait) {
    let timeout;

    return function() {
        if (!timeout) {
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            func.apply(this, arguments)
        }
    }
}
  1. 使用时间戳【只有头】
    (触发事件时立即执行,以后每过wait秒之后才执行一次,并且最后一次触发事件若不满足要求不会被执行)
// 第一版
function throttle(func, wait) {
    let previous = 0;//这里是不是Date.now没关系,这儿绑定时就会执行

    return function() {
        let now =  Date.now();
        let context = this;
        let args = arguments;
        if (now - previous >= wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}
  1. 使用定时器
    ①【只有尾】
    (第一次触发时不会执行,而是在wait秒之后才执行,当最后一次停止触发后,还会再执行一次函数。)
// 第二版
function throttle(func, wait) {
    let timeout;

    return function() {
        let context = this;
        let args = arguments;
        if (!timeout) {
            timeout = setTimeout(function(){
                timeout = null;
                func.apply(context, args)
            }, wait)
        }
    }
}

②【只有头】
△其实一般节流需求只有一端就好了,记这个为模版吧

function throttle(func, wait) {
    let timeout;

    return function() {
        if (!timeout) {
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            func.apply(this, arguments)
        }
    }
}
  1. 时间戳&定时器(有头有尾
// 方法三:时间戳 & 定时器
function throttle(fn, delay) {
  // 初始化定时器
  let timer = null;
  // 上一次调用时间
  let prev = 0;
  // 返回闭包函数
  return function () {
    // 现在触发事件时间
    let now = Date.now();
    // 触发间隔是否大于delay
    let remaining = delay - (now - prev);
    // 保存事件参数
    const args = arguments;
    // 清除定时器
    clearTimeout(timer);
    // 如果间隔时间满足delay
    if (remaining <= 0) {
      // 调用fn,并且将现在的时间设置为上一次执行时间
      fn.apply(this, args);
      prev = Date.now();
    } else {
      // 否则,过了剩余时间执行最后一次fn
      timer = setTimeout(() => {
        fn.apply(this, args)
      }, delay);
    }
  }
}
  • 疑问1:清除定时器为什么不需要这样写?
    if (timeout) {
    clearTimeout(timeout);
    timeout = null;
    }
    理解:首先,第一次操作走进remaining<=0,立即执行 [ 这是头 ]。其次,之后的每一次操作如果都在间隔时间中间 没到时间,都会走进timer=setTimeout,这个是肯定不需要的,接着直接clearTimeout就好。在等到最后一次操作在间隔时间中间时,此时没有下次操作了就不会clearTimeout,因此,最后一次操作会在delay秒之后执行 [ 这是尾 ]。

  • 疑问2:这样写行不行
    理解:俺觉得可以,没看出啥问题…

function throttle2(fn, delay) {
    let timer = null;
    let prev = 0;
    return function () {
        let now = Date.now();
        let remaining = delay - (now - prev);
        const args = arguments;

        // 头 -- 只是第一次触发执行一次(立即执行)
        if (prev == 0 && remaining <= 0) {
            fn.apply(this, args);
            prev = Date.now();
        }

        // 第一次触发开始,都走这儿(延后执行)
        if (!timer) {
            timer = setTimeout(() => {
                timer = null
                fn.apply(this, args)
            }, delay);
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值