阅读 156

JS 应用篇(四):防抖&节流函数

概念

本质上是优化高频率执行代码的一种手段

如:浏览器的 resizescrollkeypressmousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能

为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用debounce(防抖)和throttle(节流)的方式来减少调用频率。

防抖函数(debounce)

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

看一个🌰(栗子):

//模拟一段ajax请求
function ajax(content) {
  console.log('ajax request ' + content + 'time:' + new Date().Format('HH:mm:ss'))
}

let inputa = document.getElementById('unDebounce')

inputa.addEventListener('keyup', function (e) {
    ajax(e.target.value)
})
复制代码

看一下运行结果:

1.gif

可以看到,我们只要按下键盘,就会触发这次ajax请求。不仅从资源上来说是很浪费的行为,而且实际应用中,用户也是输出完整的字符后,才会请求。所以我们想要的是用户全部输出完后,我们才会发起请求,而不是在输入过程中发起请求。这里就需要用到防抖函数

简单版实现方式

function debounce(func, wait) {
    let timeout;

    return function () {
        let context = this; // 保存this指向
        let args = arguments; // 拿到event对象

        if(timeout) clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}
复制代码

立即执行实现方式

防抖如果需要立即执行,可加入第三个参数用于判断,实现如下:

function debounce(func, wait, immediate) {

  let timeout

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

    if (timeout) clearTimeout(timeout) // timeout 不为null
    if (immediate && !timeout) {
      timeout = setTimeout(function () {
        timeout = null
      }, wait)
      func.apply(context, args)
    } else {
      timeout = setTimeout(function () {
        timeout = null
        func.apply(context, args)
      }, wait)
    }
  }
}
复制代码

回顾我们刚刚的🌰,看下加入防抖函数后的效果:

//模拟一段ajax请求
function ajax(content) {
  console.log('ajax request ' + content + 'time:' + new Date().Format('HH:mm:ss'))
}

let inputb = document.getElementById('debounce')

let debounceAjax = debounce(ajax, 500)

inputb.addEventListener('keyup', function (e) {
        debounceAjax(e.target.value)
    })
复制代码

看一下运行结果: 2.gif 可以看到,我们加入了防抖以后,当你在频繁的输入时,并不会发送请求,只有当你在指定间隔内没有输入时,才会执行函数。如果停止输入但是在指定间隔内又输入,会重新触发计时。

个人理解 函数防抖就是法师发技能的时候要读条,技能读条没完再按技能就会重新读条。

节流函数(throttle)

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

节流函数实现的方式有时间戳定时器两种方式

时间戳实现方式

function throttle(func, delay) {
  var lastTime = 0;
  return function() {
    var context = this;
    var args = arguments;
    var nowTime = new Date().getTime();
    if (nowTime > lastTime + delay) {
      func.apply(context, args)
      lastTime = nowTime;
    }
  }
}
复制代码

时间戳的方式,函数在时间段开始时立即执行(lastTime设置为0),如果lastTime设置为nowTime,则第一次执行在delay秒后。

缺点:假定函数间隔1s执行,如果最后一次停止触发,卡在4.2s,则不会再执行。

定时器实现方式

function throttle(func, delay) {
  var timeout;
  return function() {
    var context = this;
    var args = arguments;
    if (!timeout) {
      setTimeout(function(){
        func.apply(context, args);
        timeout = null;
      }, delay)
    }
  }
}
复制代码

定时器的方式,函数在时间段结束时执行。可理解为函数并不会立即执行,而是等待延迟计时完成才执行。 (由于定时器延时,最后一次触发后,会再执行一次回调函数)

时间戳 + 定时器(互补优化)

function throttle (func, delay) {
  let timeout
  let previous = 0
  return function () {
    let context = this
    let args = arguments
    let now = new Date().getTime()
    // 距离下次函数执行的剩余时间
    let remaining = delay - (now - previous)
    
    // 如果无剩余时间或系统时间被修改
    if (remaining <= 0 || remaining > delay) {
      // 如果定时器还存在则清除并置为null
      if (timeout) {
        clearTimeout(timeout)
        timeout = null
      }
      
      // 更新对比时间戳并执行函数
      previous = now
      func.apply(context, args)
    } else if (!timeout) {
      // 如果有剩余时间但定时器不存在,则设置定时器
      // remaining毫秒后执行函数、更新对比时间戳
      // 并将定时器置为null
      timeout = setTimeout(() => {
        previous = new Date().getTime()
        timeout = null
        func.apply(context, args)
      }, remaining)
    }
  }
}
复制代码

看一个🌰:

//模拟一段ajax请求
function ajax(content) {
  console.log('ajax request ' + content + 'time:' + new Date().Format('HH:mm:ss'))
}

let throttleAjax = throttle(ajax, 1000)

let inputc = document.getElementById('throttle')
inputc.addEventListener('keyup', function(e) {
    throttleAjax(e.target.value)
})
复制代码

看一下运行结果: 4.gif 可以看到,我们在不断输入时,ajax会按照我们设定的时间,每1s执行一次。

个人理解 函数节流就是fps游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。

总结

相同点

  • 都可以通过使用 setTimeout 实现
  • 目的都是,降低回调执行频率。节省计算资源

不同点

  • 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout和 setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次

例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数:
节流:每隔 500ms 就执行一次。
防抖:则不管调动多少次方法,在2s后,只会执行一次

6.png

应用场景

  • debounce防抖在连续的事件,只需触发一次回调的场景有

    • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
    • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
  • throttle节流在间隔一段时间执行一次回调的场景有:

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

扩展

防抖节流函数在TypeScript下怎么写

标准的防抖函数代码

function debounce(func, wait) {
    let timeout;

    return function () {
        let context = this; // 保存this指向
        let args = arguments; // 拿到event对象

        if(timeout) clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}
复制代码

改造成ts代码 加上对应的类型描述后,发现编译不通过: 7.png 参考:www.typescriptlang.org/docs/handbo…

TypeScript 提供了一种机制,可以在函数入参列表中第一个位置处,手动写入 this 标识其类型。但这个 this 入参只作为一个形式上的参数,供 TypeScript 做静态检查时使用,编译后是不会存在于真实代码中的。 8.png

修正ts下this参数的问题 9.png 问题解决!!

参考

7分钟理解JS的节流、防抖及使用场景
面试官:什么是防抖和节流?有什么区别?如何实现?
Debounce防抖函数在TypeScript下怎么写

文章分类
前端
文章标签