0%

防抖和节流

防抖函数典型的案例就是优化输入搜索请求,我在一个vue学习项目中也使用过,用来优化子组件每个图片加载完成后,给父组件发送通信。以为自己用了就懂了,突然再看到时,发现有几个地方不懂,再次学习解惑疑惑点。

防抖

定义

在事件触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

实现原理

debounce函数防抖的基本思想就是设置一个定时器timer,只有在等待指定时间后,才会执行请求操作函数fn;如果在没有等待到指定时间,再一次触发debounce函数,就会清除计时器timer,并重新设置一个定时器。

代码实现

function debounce1(func, delay) {
    let timer = null
    return function(...args) {
        if (timer) {
            clearTimeout(timer)
        }
        // let context = this
        timer = setTimeout(() => {
            func.apply(this, args);
        }, delay)
    }
}

下面就是我的疑惑点:

  • 高阶函数return function{}
  • timer,为什么能清除上一次的timer
  • 为什么要改变this指向

先写一个demo

<div class="box"></div>
<script type="text/javascript">
    let counter = 0

    let box = document.getElementsByClassName('box')[0]

    function doSomethings() {
        counter++
        box.innerText = counter
    }
    box.onmousemove = doSomethings
</script>

上面的代码,当鼠标在box上面移动的时候,counter的数字就会加一。

如果想让鼠标移动后只有等待1秒后,counter的数字才会加一。

这样就可以初步写个函数

function debounce(func) {
    let timer = null
    return function() {
        if (timer) {
            clearTimeout(timer)
        }
        timer = setTimeout(function() {
            func()
        }, 1000)
    }
}
box.onmousemove = debounce(counterAdd)

使用return function高阶函数实现闭包,使timer变量保存,这样每触发一次debounce函数,返回一个定时器函数,但使用的都是一个timer,也就能实现清除上一次的计时,再新开一个计时器。


// 打印this,和事件
function doSomethings(e) {

    box.innerText = counter++
    console.log(this)
    console.log(e)
}

如果在函数doSomethings中使用了this,dom节点触发事件的时候this应该是指向自身的,但是打印却发现指向window。而事件event应该是MouseEvent ,打印结果确实undefined。

原因是在setTimeout中,this都是指向window的,

所以应该在setTimeout之前保存this,然后使用apply修改this指向;使用...args保存传入的事件数组,apply的第二个参数传入doSomethings函数中。

但是由于setTimeout函数使用的是箭头函数,this指向是上下文this,所以指向不是windows

最终的bounce函数代码如下

function debounce(func, delay) {
    let timer = null
    return function(...args) {
        if (timer) {
            clearTimeout(timer)
        }
        let context = this
        timer = setTimeout(() => {
            func.apply(context, args);
        }, delay)
    }
}

进一步优化

在undercore.js函数库中,_.debounce有三个参数(func, wait, immediate)

immediate为true表示第一次触发立即执行,而不是等待wait后开始执行,触发是上沿触发,而不是尾随触发。

function debounce(func, delay, immediate) {
    let timer = null
    return function(...args) {
        if (timer) {
            clearTimeout(timer)
        }
        let context = this
        if (immediate) {
            let callNow = !timer        // 第一次是timer = null
            timer = setTimeout(() => {
                timer = null            //等待delay,设置timer = null
            }, delay)
            if (callNow) func.apply(context, args);
        } else {
            timer = setTimeout(() => {
                func.apply(context, args);
            }, delay)
        }

    }
}

取消防抖功能

function debounce(func, delay, immediate) {
    let timer = null
    let result
    let debounced = function(...args) {
        if (timer) {
            clearTimeout(timer)
        }
        let context = this
        if (immediate) {
            let callNow = !timer // 第一次是timer = null
            timer = setTimeout(() => {
                timer = null //等待delay,设置timer = null
            }, delay)
            if (callNow) {
                result = func.apply(context, args);
            }
        } else {
            timer = setTimeout(() => {
                result = func.apply(context, args);
            }, delay)
        }
    }


    debounced.cancel = function() {
        clearTimeout(timer);
        timer = null; //内存回收
    }
    return debounced;
}
let doSome = debounce(doSomethings, 5000)
box.onmousemove = doSome
btn.onclick = function() {
    doSome.cancel()
}

应用场景

  • scroll事件的滚动触发
  • 搜索框输入查询
  • 表单验证
  • 按钮提交事件

节流

原理:如果你持续触发事件,每隔一段时间,只执行一次事件

代码实现

// 时间戳 -- 第一次会触发,最后一次不会触发
function throttle(func,delay){
    let context
    // 之前的时间戳
    let previous = 0
    return function(...args){
        context = this

        //获取当前的时间戳
        let now  = new Data().valueOf()
        if(now - previous >delay){
            // 立即执行
            func.apply(context,args)
            previous = now
        }
    }
}

// 定时器
function throttle(func,delay){
    let timer = null
    return function(...args){
        if(!timer){
            timer = setTimeout(() => {
                timer = null
                func.apply(this,args)
            },delay)
        }
    }
}
function debounce(func,delay){
    let timer = null 
    return function(args){
        if(timer){
            timer = clearTimeout(timer)
        }
        timer = setTimeout(() =>{
            func.call(this,...args)
        },delay)
    }
}

function throttle(func,delay){
    let timer = null
    return function(){
        if(!timer) {
            timer = setTimeout(() => {
                timer = null 
                func.call(this)
            },delay)
        }
    }
}