防抖

防抖在日常项目开发的时候还是很重要的,用于防止函数的重复触发,节省性能开支。

比如监听用户的input输入进行api查询,由于input事件每次改变输入的内容都会触发,但是一般来说,只有最后一次input才有效,所以会经常采用防抖的形式来触发。

防抖:在一定时间内重复触发之后只会生效最后一次。

如何写一个防抖呢,我们慢慢来

< script >
"use strict"
const input = document.querySelector("input");

input.addEventListener("input", debounce(function() {
    console.log(`触发了:${Date.now()}`, this);
}, 300))

function debounce(fn, delay) {
    let timer = null;
    return function() {
        if (timer) clearTimeout(timer);
        timer = setTimeout(fn, delay)
    }
} <
/script>

创建一个debounce函数,函数内新增一个timer变量,用于存放定时器,然后抛出一个新函数,函数里面判断,如果定时器存在,就先清除定时器,重新赋值一个定时器。

这样一个基本的雏形就有了,剩下的就是要考虑函数this,以及参数的问题了。

this指向

函数内运行的函数,其this指向window对象,如果在严格模式下,this就是undefined。

function fn1() {
  function fn2() {
    console.log(this)
  }

  fn2();
}

fn1();   //window,严格模式下undefined

但是我们的防抖,是将函数return出去,所以,按照防抖的写法,return出去的函数直接绑定再input对象上,那么它的this指向的就是input本身。

< script >
"use strict"
const input = document.querySelector("input");

input.addEventListener("input", debounce(function() {
    console.log(`触发了:${Date.now()}`, this);
}, 300))

function debounce(fn, delay) {
    let timer = null;
    return function() {
        console.log(this);   //input
        if (timer) clearTimeout(timer);
        timer = setTimeout(fn, delay)
    }
} 
<  /script>

但是我们实际的函数fn却是被setTimeout调用,setTimeout存在于window对象上,fn通过作用域链往上查询this,最终找到的是window,this就等于window。

在函数中,this的指向完全取决于函数调用的位置;谁调用它,他就指向调用它的那个对象。

那么,既然能在return的函数中正确的获取到this,那么我们就可以有很多种方式让fn绑定到正确的this上。

< script >
"use strict"
const input = document.querySelector("input");

input.addEventListener("input", debounce(function() {
    console.log(`触发了:${Date.now()}`, this);
}, 300))

function debounce(fn, delay) {
    let timer = null;
    return function() {
        if (timer) clearTimeout(timer);
        timer = setTimeout(fn.bind(this), delay)
    }
} 
</script>

我们可以用bind或者call,apply的方式改写this指向。

你甚至可以看到防抖中这种写法:

< script >
"use strict"
const input = document.querySelector("input");

input.addEventListener("input", debounce(function() {
    console.log(`触发了:${Date.now()}`, this);
}, 300))

function debounce(fn, delay) {
    let timer = null;
    return function() {
        let that = this
        if (timer) clearTimeout(timer);
        timer = setTimeout(()=>{
          fn.call(that);
        }, delay)
    }
} <
/script>

利用that变量保存,然后在箭头函数里面用call这种方式,事实上其实没必要的,箭头函数本身没有this,他的this将是声明它时作用域链中的this,那么this也会是input,因为他会找到return function它的this。

现在this,问题我们高定了,剩下的就是参数问题了。

参数

事件绑定,一般都是会使用到event对象,甚至一些其他自定义参数,我们不能确定使用者它将使用什么参数名,多少个参数,所以,我们得有一个办法获取到函数接收的所有参数。

在es5的时候,我们会使用函数内的arguments对象,他是一个类数组对象,里面包含了所有的参数,但是并不建议使用,不过我也是道听途说,说是会影响性能。既然都es6了,可以放弃了。

现在es6的话,我们可以利用函数的reset参数,将所有参数保存到数组中去。

function fn(...args) {
  console.log(args);   //[...]
}

那么现在参数可以拿到了,如何传给fn呢,我们bind方法其实提供了第二个参数,他和call的第二个参数一样,传递参数用的,所有参数用逗号分割。所以

< script >
"use strict"
const input = document.querySelector("input");

input.addEventListener("input", debounce(function(evnet) {
    console.log(`触发了:${Date.now()}`, this, evnet);
}, 300))

function debounce(fn, delay) {
    let timer = null;
    return function(...args) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(fn.bind(this,...args), delay)
    }
} 
</script>

一个防抖函数就完成了。

注意事项

由于防抖生成的是全新的函数,如果绑定了事件,想要关闭事件,那么就必须先将防抖生成的函数变量保存下来才能成功清除事件。

<script>
"use strict"
const input = document.querySelector("input");
const button = document.querySelector("button");

const fn = debounce(function(event) {
    console.log(`触发了:${Date.now()}`, this, event);
}, 300);

input.addEventListener("input", fn);
button.addEventListener("click", function(event) {
    input.removeEventListener("input", fn);
});

function debounce(fn, delay) {
    let timer = null;
    return function(...args) {
        console.log(this);
        if (timer) clearTimeout(timer);
        timer = setTimeout(fn.bind(this, ...args), delay)
    }
} 
</script>

节流

节流和防抖的处理完全不一样,节流是在一定时间内只触发一次,不管点击多少次。

用一个示意图来表明两者之间的区别:

画的比较糙,但是一眼就能看明白,节流在指定时间内触发一次,而防抖只触发最后一次。

function throttle(fn, delay) {
    let timer = null;
    return function(...args) {
        if (timer) return;
        timer = setTimeout(() => {
            fn.apply(this, args);
            timer = null;
        }, delay);
    };
}

当然还有一些简单的写法,比如下面这种:

function throttle(fn, delay) {
    let lastTime = null;

    return function(...args) {
        const newTime = Date.now();
        if (!lastTime || newTime - lastTime > delay) {
            fn.call(this, ...args);
            lastTime = newTime;
        }
    }
}

这种写法会有一个bug,就是最后一次没法触发,除非触发它刚好在delay的时间内,所以也不是很建议使用,是我之前自己理解的一种写法,作为经验贴在这吧!

注意事项

由于会生成新的函数,所以事件绑定的时候,记得用变量保存,方便正确清理事件。

分类: JavaScript 标签: javascript防抖节流debouncethrottle

评论

暂无评论数据

暂无评论数据

目录