防抖与节流

本文深入探讨了前端领域的防抖与节流技术,通过实例解释了这两种技术的工作原理,包括事件触发的处理方式,如用户输入、键盘事件等,并提供了简易版的实现代码,同时讨论了如何优化防抖函数,使其更符合实际应用需求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言


防抖与节流是前端领域不可或缺的知识点。刚开始学习的时候容易搞混这两个概念。我们先来自己实现一下,然后再分析lodash的实现方式。

防抖

事件触发后的一段时间内没有再次触发事件,绑定的函数才会执行。

常见的场景有

  • 用户输入内容,实时搜索
  • 键盘的按下抬起事件
  • 鼠标移动事件
  • 滚动条移动事件

这些事件的触发比较密集,很容易造成浏览器卡顿。

用图来表示一下
在这里插入图片描述
上图中

  • A事件延迟期间,B事件触发,则A事件在延迟结束后不会执行函数。
  • B事件在延迟结束后会正常执行函数
  • C事件在延迟结束后会正常执行函数
简易版实现

我们假设每次事件的触发都是有效的(延迟结束后执行函数),后续的事件触发会把上一次的事件置为无效(取消定时器)。

//简易版实现
  function myDebounce1(callback,duration){
    let timerId
    return function(){
      if(timerId!== undefined){
        clearTimeout(timerId)
      }
      timerId = setTimeout(function(){
        callback()
        },duration)
    }
  }

简易版实现是远远不够的,作为调用方来讲,调用防抖函数应该跟直接绑定执行函数一样方便。我们进一步优化一下。

更改this指向

原执行函数内部的this指向的应该是触发事件的元素

//更改this指向
  function myDebounce2(callback,duration){
    let timerId,context
    return function(){
      //这里的this指向的是事件触发元素
      context = this
      if(timerId!== undefined){
        clearTimeout(timerId)
      }
      timerId = setTimeout(function(){
        //apply更改函数的this指向
        callback.apply(context)
      },duration)
    }
  }
传入参数

事件执行函数的第一个参数应该是event对象,下面的两种方式效果是一样的。

//传入参数
  function myDebounce3(callback,duration){
    let timerId,context,arg
    return function(){
      context = this
      //arguments 里包含了传入的参数
      arg = arguments
      if(timerId!== undefined){
        clearTimeout(timerId)
      }
      timerId = setTimeout(function(){
        //apply指定参数
        callback.apply(context,arg)
      },duration)
    }
  }

另一种写法

 //传入参数 另一种写法
   function myDebounce4(callback,duration){
    let timerId,context
    return function(event){
      context = this
      if(timerId!== undefined){
        clearTimeout(timerId)
      }
      timerId = setTimeout(function(){
        //apply指定参数
        callback.call(context,event)
      },duration)
    }
  }
立即执行

有时候我们希望第一次事件触发要立即执行
还是借用这张图
在这里插入图片描述
立即执行的情况下变成

  • A事件触发时执行函数
  • B事件触发时,A事件的延迟事件还未结束,所以B事件不执行函数
  • C事件触发时执行函数

可以看到,立即执行情况下,上次的事件会阻止后续的事件执行函数,和延迟执行正好相反。事件延迟期间,新的事件会延长等待时间。

//立即执行
  function myDebounce5(callback,duration,immdiate=false) {
    let timerId,context,flag
    //是否立即执行
    return function(event) {
      context=this
      if(timerId!==undefined) {
        clearTimeout(timerId)
      }
      if(immdiate) {
        //事件触发的同时,持有timerId直到延迟结束
        if(timerId===undefined) {
          callback.call(context,event)
        }
        //上一次事件延迟结束,释放timerId,后续的事件才能触发函数
        timerId=setTimeout(function() {
          clearTimeout(timerId)
          timerId=undefined
        },duration)
      } else {
        timerId=setTimeout(function() {
          callback.call(context,event)
        },duration)
      }
    }
  }
取消

如果我们想随时取消防抖,就需要对外暴露方法。
对于立即执行方式来说,取消可以使下次事件必定执行函数
对于延迟执行方式来所,取消可以让有效事件变成无效事件

 //增加取消机制
  function myDebounce6(callback,duration,immdiate=false) {
    let timerId,context
    //是否立即执行
    return function(event) {
      context=this
      if(timerId!==undefined) {
        clearTimeout(timerId)
      }
      if(immdiate) {
        //事件触发的同时,持有timerId直到延迟结束
        if(timerId===undefined) {
          callback.call(context,event)
        }
        //上一次事件延迟结束,释放timerId,后续的事件才能触发函数
        timerId=setTimeout(function() {
          clearTimeout(timerId)
          timerId=undefined
        },duration)
      } else {
        timerId=setTimeout(function() {
          callback.call(context,event)
        },duration)
      }
    }
    //取消防抖
    debounced.cancel=function() {
      if(timerId!==undefined) {
        clearTimeout(timerId)
      }
      context=timerId=undefined
    }
    return debounced
  }

节流

每隔一段时间,才会执行一次函数。把高频无效的触发转为有效的低频触发。
同样用图说明
在这里插入图片描述

节流的实现方式,有时间戳方式和计时器方式

时间戳方式

判断上次事件触发距离现在的时间间隔是否大于指定时间间隔。
首次触发会立即执行,事件延迟期间,不会有新的函数执行。
对于上图:

  • A事件在触发时执行函数
  • B事件不会执行函数
  • C事件在触发时执行函数
//时间戳方式
  function myThrottle1(callback,duration) {
    let dateObj=new Date()
    let begin,context,arg
    return function() {
      context = this
      arg = arguments
      let dateObj=new Date()
      let current=dateObj.getTime()
      if(begin === undefined || current-begin>=duration) {
        callback.apply(context,arg)
        begin=current
      }
    }
  }
计时器方式

首次触发会创建计时器,首次并不会立即执行。延迟时间结束后会执行函数。在延迟期间不会创建新的计时器。
对于上图:

  • A事件在延迟结束时触发函数
  • B事件不会创建计时器,也不会执行函数
  • C事件在延迟结束时触发函数
//计时器方式
  function myThrottle2(callback,duration){
    let timerId,context,arg
    return function(){
      context = this
      arg = arguments
      if(timerId === undefined){
        timerId = setTimeout(function(){
          callback.apply(context,arg)
          timerId = undefined
          clearTimeout(timerId)
        },duration)
      }
    }
  }
优化

上面两种方式的问题在于。B事件不会执行函数。我们可以做到,A事件立即执行,B事件在A事件延迟结束后立即执行,同样C事件在B事件延迟结束后立即执行,这样更加平滑,用户不会感受到事件确实。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

csw_coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值