防抖和节流扩展 -- 关于闭包、this

194 阅读6分钟
  • 参考文档1讲解不错,结合参考文档2一起看;
  • 本文另补充了在防抖和节流闭包中,分别使用箭头函数、普通函数中的this指向问题
  • 不管防抖还是节流,要记得用闭包来保存定时器;

参考文档1:github.com/Advanced-Fr…

参考文档2:q.shanyue.tech/fe/code/3.h…

防抖

  • 触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间
  • 思路:每次触发事件时都取消之前的延时调用方法
// github版 --- 看这个
function debounce(fn) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function () {
    clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
    // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
    timeout = setTimeout(() => { 
       console.log('arguments', arguments)
      fn.apply(this, arguments);  // arguments是指向我也不知道???
    }, 5000);
  };
}

function sayHi(a) {
  console.log('防抖成功');
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi)); // 防抖    
// arg 加了参数版
function debounce(fn, arg) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function () {
    clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
    // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
    timeout = setTimeout(() => { 
        console.log('---arg', this, arg) // this:input
        fn.apply(this, [arg]);
    }, 500);
  };
}
function sayHi(arg) {
  console.log('防抖成功', arg);
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi, {a:'aaaa'})); // 防抖

节流

  • 高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率
  • 思路:每次触发事件时都判断当前是否有等待执行的延时函数
// git版 -- 看这个
function throttle(fn) {
      let canRun = true; // 通过闭包保存一个标记
      return function () {
        if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
        canRun = false; // 立即设置为false
        setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
          fn.apply(this, arguments);
          // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。
          // 当定时器没有执行的时候标记永远是false,在开头被return掉
          canRun = true;
        }, 500);
      };
    }
    function sayHi(e) {
      console.log(e.target.innerWidth, e.target.innerHeight);
    }
    window.addEventListener('resize', throttle(sayHi));
// blog版,直接通过 timer来判断
function throttle (f, wait) {
  let timer
  return (...args) => {
    if (timer) { return }
    timer = setTimeout(() => {
      f(...args)
      timer = null
    }, wait)
  }
}

通过节流引发的 this指向总结大全

  • 代码1)中this指向 input --- 因为debounce的返回值其实就是 inp.addEventListener('input')的回调函数,事件处理函数中的 this指向 DOM
function debounce(fn, arg) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function () {
    console.log('---1this', this) //1) this指向 input

    clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉

    // 2.1) 写法一 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
    timeout = setTimeout(() => { 
    
    // 2.2)写法二  然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
    // timeout = setTimeout(function() { 

        console.log('---2this', this, arg) // 3)

        fn.apply(this, [arg]);  // 4.1) 写法二
        // fn(arg) // 4.2) 写法一

    }, 500);

    
  };
}
function sayHi(arg) {
  console.log('---3this防抖成功', this, arg);  // 5)
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi, {a:'aaaa'})); // 防抖

// 解析:
// 1) this指向 input --- 因为debounce的返回值其实就是 inp.addEventListener('input')的回调函数,事件处理函数中的 this指向 DOM

// 2.1)会导致 3)this指向 input --- 因为箭头函数的this指向箭头函数定义时上层作用域中的this,而不是使用时所在的对象
// 2.2) 会导致 3)this指向 window --- 因为普通函数执行时 setTimout中的 this指向window -- 
// 原因:把setTimout中的回调看成单独的fn,未做绑定,看成在window下执行

// 4.1) 结合 2.1)会导致 5) 指向 input --- 因为 2.1)执行后,导致 3)this指向input, 此时this被 apply绑定了,5) 指向input
// 4.1) 结合 2.2)会导致 5) 指向 window --- 因为 this 在2.2)就指向了 window,apply绑定的是 window
// 4.2) 不论结合 2.1)还是 2.2)都会导致 5) 指向 window --- 因为未做任何绑定,默认this指向window

1 当使用了箭头函数 和 apply

  • 2.1)会导致 3)this指向 input --- 因为箭头函数的this指向箭头函数定义时上层作用域中的this,而不是使用时所在的对象
  • 4.1) 结合 2.1)会导致 5) 指向 input --- 因为 2.1)执行后,导致 3)this指向input, 此时this被 apply绑定了,5) 指向input

小结一下:

  • 使用箭头函数:为了箭头函数体里的this指向input; 使用普通函数this指向window了
  • 使用apply:加上 apply 确保 在 sayHi 函数里的 this 指向的是 input对象(不然就指向 window 了,不是我们想要的);
  • 上行补充 ~~~ 因为sayHi函数是在全局中调用运行,所以this指向了window,所以才需要加上apply,显示绑定this值(input对象)到sayHi函数里面去
function debounce(fn, arg) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function () {
    console.log('---1 this', this) //1) this 指向 input

    clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉

    timeout = setTimeout(() => { // 2.1) 

        console.log('---', this, arg) // 3) this指向 input

        fn.apply(this, [arg]);  // 4.1) this与上行this一致,此时 this指向 input

    }, 500);

    
  };
}
function sayHi(arg) {
  console.log('防抖成功', this, arg);  // 5) this 指向 input
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi, {a:'aaaa'})); // 防抖

2 当使用了普通函数 和 apply

  • 2.2) 会导致 3)this指向 window --- 因为普通函数执行时 setTimout中的 this指向window -- 原因:把setTimout中的回调看成单独的fn,未做绑定,看成在window下执行
  • 4.1) 结合 2.2)会导致 5) 指向 window --- 因为 this 在2.2)就指向了 window,apply传的this就是 window
function debounce(fn, arg) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function () {
    console.log('---1this', this) //1) 3) this指向 input

    clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉

    timeout = setTimeout(function() { // 2.2) 

        console.log('---2this', this, arg) // 3) this指向 window

        fn.apply(this, [arg]);  // 4.1)  this与上行中的this一样,指向 window

    }, 500);

    
  };
}
function sayHi(arg) {
  console.log('---3this防抖成功', this, arg);  // 5) this指向 window
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi, {a:'aaaa'})); // 防抖

3 当不使用 apply

    1. this 指向 window;因为 4.2)未绑定this
  • 不论结合 2.1)还是 2.2),影响的只是setTimout里的this,即3)
function debounce(fn, arg) {
      let timeout = null; // 创建一个标记用来存放定时器的返回值
      return function () {
        console.log('---1this', this) //1) this指向 input

        clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉

        timeout = setTimeout(() => { // 2.1) 
        // timeout = setTimeout(function() { // 2.2)

            console.log('---2this', this, arg) // 3) 2.1)执行时 this指向 input;2.2)执行时 this指向 window

            fn(arg) // 4.2) 该行未绑定this

        }, 500);

        
      };
    }
    function sayHi(arg) {
      // 5) this 指向 window, 因为 4.2)未绑定this, 不论结合 2.1)还是 2.2),该行this都为window
      console.log('---3this防抖成功', this, arg);  
    }

    var inp = document.getElementById('inp');
    inp.addEventListener('input', debounce(sayHi, {a:'aaaa'})); // 防抖