实现防抖和节流

一、认识防抖和节流函数

  • 防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动
    • 而javaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理的。
    • 而对于某些频繁的事件会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生

如果想要在500ms内,无论用户点击了多少次,最后只执行一次。

  • 防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题
  • 但是很多前端开发者面对这两个功能,有点摸不着头脑
    • 某些开发者根本无法区分和防抖和节流有什么区别(面试经常会被用到)
    • 某些开发者可以区分,但是不知道如何应用
    • 某些开发者会通过一些第三方库来使用,但是不知道内部原理,更不会编写。

1、认识防抖debounce函数

我们用一副图来理解一下它的过程

  • 当事件触发时,相应的函数并不会立即触发 ,而是会等待一定的时间
  • 当事件密集触发时,函数的触发会被频繁的推迟。
  • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数。
image-20220525152118119.png

防抖的应用场景很多:

  • 输入框中频繁的输入内容,搜索或者提交信息
  • 频繁的点击按钮,触发某个事件
  • 监听浏览器滚动事件,完成某些特定操作
  • 用户缩放浏览器的resize事件。
  • 【手游王者“回城”】

1.1 防抖函数的案例

  • 我们都遇到过这样的场景,在某个输入框中输入自己想要搜索的内容:

  • 比如想要搜索一个MacBook

    • 当我们输入M时,为了更好的用户体验,通常会出现对应的联想内容,这些联想内容通常是保存在服务器的,所以需要一次网络请求。
    • 当继续输入Ma时,再次发送网络请求
    • 那么MacBook一共会发送7次网络请求
    • 这大大损耗了我们整个系统的性能,无论是前端的事件处理,还是
  • 但是我们需要这么多次的网络请求吗?

    • 不需要,正确的做法应该是在合适的情况下再发送一次网络请求。
    • 比如如果用户快速的输入一个macbook,那么只是发送一次网络请求。
    • 比如如果用户输入一个M想了一会儿,这个时候m确实应该发送一次网络请求。
    • 也就是我们应该监听用户在某个时间,比如500ms内,没有再触发事件时,再发送网络请求。

1.2 案例准备

  • 我们通过一个搜索框来延迟防抖函数的实现过程。
    • 监听input的输入,通过打印模拟网络请求。
  • 测试发现快速输入一个macbook共发送了7次请求,显示我们需要对它进行防抖操作。
 <input type="text">
  <script>
    const inputEl = document.querySelector("input");
    let count = 0;
    inputEl.oninput = () => {
      console.log(`发送了${++count}网络请求。`);
    }
  </script>
image-20220525204726181.png

1.3 UnderScore库的介绍

  • 事实上我们可以通过第三方库来实现防抖操作

  • 这里使用underscore

    • 我们可以理解成lodash是underscore的升级版,它更重量级,功能也更多。
    • 但是目前我看到underscore还在维护,lodash已经很久没有更新了。
  • 三种方式引入underscore

    • 从github里面下载下来
    • 使用script标签引入
    • 使用npm install,然后导入使用
  <input type="text">
  <script src="https://cdn.jsdelivr.net/npm/underscore@1.13.3/underscore-umd-min.js"></script>
  <script>
    const inputEl = document.querySelector("input");
    let count = 0;
    const inputChange = () => {
      console.log(`发送了${++count}网络请求。`);
    }
    // 防抖操作,如果频繁触发事件,响应函数会不断被推迟执行。
     inputEl.oninput = _.debounce(inputChange, 2000)

  </script>

1.4 防抖函数v1 (实现了this、参数)


function debounce(fn,delay){
  // * 定义了一个定时器,保存上一次的定时器
  let timer=null;

  // * 真正执行的函数
  const _debounce=function(...args){
    // * 如果上一次已经设置了定时器,就将上一次的定时器取消,
    if(timer) clearTimeout(timer)
    timer=setTimeout(()=>{
      // * 外部传入要真正执行的函数
     fn.apply(this,args)
    },delay)
  }
  return _debounce
}

/**
 * * 存在问题,
 * * 1.this应该指向的元素本身 :在传入函数调用的时候使用 fn.apply(this)
 * * 2.还存在event对象:使用剩余参数进行接收 fn.apply(this.args)
 */

1.5 防抖函数v2 (第一次实现立即执行)

  • 有人希望第一次的时候应该立即执行,有些希望第一次不立即执行,所以可以设置一个参数

    • 如果设置第一次立即执行

      • 在第一次输入的时候立即执行,在执行了一次响应函数后,后面再次触发应该也要立即执行。
        • 在执行函数的时候会先判断immediate是否为true,而且前一次isInvoke是否已经调用过,如果immediate为true并且没有调用过,就去立即执行这个函数,将isInvoke设置为true
        • 在第二次调用的时候,将isInvoke设置为false。
    • 注意:不要轻易修改参数


function debounce(fn,delay,immediate=false){
  // * 定义了一个定时器,保存上一次的定时器
  let timer=null;
  let isInvoke=false;
  // * 真正执行的函数
  const _debounce=function(...args){
    // * 如果上一次已经设置了定时器,就将上一次的定时器取消,
    if(timer) clearTimeout(timer)

    // * 判断是否需要 立即执行
    if(immediate &&!isInvoke) {
      fn.apply(this,args);
      isInvoke=true;
    }else{
      timer=setTimeout(()=>{
        // * 外部传入要真正执行的函数
       fn.apply(this,args)
       isInvoke=false;
      },delay)
    }
  }
  return _debounce
}

/**
 * * 有人希望第一次的时候应该立即执行,有些希望第一次不立即执行,所以可以设置一个参数
 * * 实现细节:
 * * 如果设置第一次立即执行:
 * *      1.在第一次输入的时候立即执行,在执行了一次响应函数后,后面再次触发应该也要立即执行。
 * *                在执行函数的时候会先判断immediate是否为true,而且前一次是否已经调用过,如果immediate为true并且前一次已经调用过就去立即执行这个函数
 * *                如果不是第一次调用,在函数调用的时候,就设置为false
 * 
 * *      2. 注意:外部传进来的参数,不要轻易去修改。
 */

1.6 防抖函数v3(实现取消功能)

  • 将定时器取消,将timer和isInvoke设置初始值。
image-20220525230051005.png
 <input type="text"> <button>取消</button>
  <script src="./03-debounce-v3-取消功能.js"></script>
  <script>
    const inputEl = document.querySelector("input");
    let count = 0;
    const inputChange = function (e) {
      // * 这个this.应该是这个元素对象
      console.log(`发送了${++count}网络请求。`, this, e);
    }

    const debounceChange = debounce(inputChange, 3000, true)
    inputEl.oninput = debounceChange

    // * 取消功能
    const cancelBtn = document.querySelector("button")
    cancelBtn.onclick = debounceChange.cancel

  </script>

function debounce(fn,delay,immediate=false){
  // * 定义了一个定时器,保存上一次的定时器
  let timer=null;
  let isInvoke=false;
  // * 真正执行的函数
  const _debounce=function(...args){
    // * 如果上一次已经设置了定时器,就将上一次的定时器取消,
    if(timer) clearTimeout(timer)

    // * 判断是否需要 立即执行
    if(immediate &&!isInvoke) {
      fn.apply(this,args);
      isInvoke=true;
    }else{
      timer=setTimeout(()=>{
        // * 外部传入要真正执行的函数
       fn.apply(this,args)
       isInvoke=false;
      },delay)
    }
  }

  // * 封装取消功能
   _debounce.cancel=function(){
     console.log("我执行了cancel");
     if(timer) clearInterval(timer)
     timer=null;
     isInvoke=false;
   }

  return _debounce
}

/**
 * * 如果用户突然点击了取消,应该是要取消函数执行,而不是继续执行函数,这会浪费一些资源。
 * 
 */

1.7 防抖函数v4(将执行的函数的返回值返回)

  • 可以给多设置一个参数(回调函数),在有值的时候调用这个回调函数。
1.7.1 使用一个回调函数来解决
<input type="text"> <button>取消</button>
  <script src="./04-debounce-v4-函数返回值.js"></script>
  <script>
    const inputEl = document.querySelector("input");
    let count = 0;
    const inputChange = function (e) {
      // * 这个this.应该是这个元素对象
      console.log(`发送了${++count}网络请求。`, this, e);
      return "aaaaa"
    }

    const debounceChange = debounce(inputChange, 2000, true, res => {
      console.log("真正执行函数的返回值:", res);
    })
    inputEl.oninput = debounceChange

    // * 取消功能
    const cancelBtn = document.querySelector("button")
    cancelBtn.onclick = debounceChange.cancel

  </script>

function debounce(fn,delay,immediate=false,resultCallback){
  // * 定义了一个定时器,保存上一次的定时器
  let timer=null;
  let isInvoke=false;
  // * 真正执行的函数
  const _debounce=function(...args){
    // * 如果上一次已经设置了定时器,就将上一次的定时器取消,
    if(timer) clearTimeout(timer)

    // * 判断是否需要 立即执行
    if(immediate &&!isInvoke) {
      const result=fn.apply(this,args);
      if(resultCallback) resultCallback(result)
      isInvoke=true;
    }else{
      timer=setTimeout(()=>{
        // * 外部传入要真正执行的函数
       const result=fn.apply(this,args)
       if(resultCallback) resultCallback(result)
       isInvoke=false;
      },delay)
    }
  }

  // * 封装取消功能
   _debounce.cancel=function(){
     console.log("我执行了cancel");
     if(timer) clearInterval(timer)
     timer=null;
     isInvoke=false;
   } 

  return _debounce
}

/**
 * * 怎么将响应函数的返回值返回出去
 */
1.7.2 使用Promise

2. 认识节流throttle函数

  • 我们用一副图来理解一下节流的过程
    • 当事件触发时,会执行这个事件的响应函数
    • 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数。
    • 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的。
image-20220525161638228.png
  • 节流的应用场景:
    • 监听页面的滚动事件。
    • 鼠标移动事件。
    • 用户频繁点击按钮操作。
    • 游戏中的一些设计。【飞机大战的操作】

2.1 第三方库的实现

  <input type="text">
  <script src="https://cdn.jsdelivr.net/npm/underscore@1.13.3/underscore-umd-min.js"></script>
  <script>
    const inputEl = document.querySelector("input");
    let count = 0;
    const inputChange = () => {
      console.log(`发送了${++count}网络请求。`);
    }

    // 节流操作 按照固定的频率进行触发响应函数
    inputEl.oninput = _.throttle(inputChange, 2000)
  </script>

2.2 节流函数的逻辑分析

  • 响应函数什么时候执行,remainTime=interval-(nowTime-lastTime)
    • 如果remainTime小于等于0,执行响应函数,并将lastTime设置为nowTime

2.3 节流函数v1-基本功能实现

 <input type="text"> <button>取消</button>
  <script src="./05-throttlev1-基本实现.js"></script>
  <script>
    const inputEl = document.querySelector("input");
    let count = 0;
    const inputChange = function (e) {
      // * 这个this.应该是这个元素对象
      console.log(`发送了${++count}网络请求。`, this, e);
      return "aaaaa"
    }

    inputEl.oninput = throttle(inputChange, 2000)
function throttle(fn,interval){
  let lastTime=0;
  const _throttle=function(...args){
    // * getTime 获取的是时间戳
    const nowTime=new Date().getTime();
    const remainTime=interval-(nowTime-lastTime);
    if(remainTime<=0){
      fn.apply(this,args);
      lastTime=nowTime
    }
  }
  return _throttle;
}

2.4 节流函数v2-第一次是否立即触发

function throttle(fn,interval,options={leading:true,trailing:false}){
  // * 记录上一次的开始时间
  let lastTime=0;
  // * 将是否第一次触发和最后一次触发取出来
  const {leading,trailing}=options;
  // * 事件触发时,真正执行的函数
  const _throttle=function(...args){
    // * 获取当前事件触发时的时间  getTime 获取的是时间戳
    const nowTime=new Date().getTime();
    // * 第一次不触发的时候,将lastTime设置为nowTime
   if(!lastTime && !leading) lastTime=nowTime;
    // * 使用当前触发的时间和上一次的开始时间、时间间隔,计算出还剩多长时间触发函数。
    const remainTime=interval-(nowTime-lastTime);
    if(remainTime<=0){
      // * 真正触发函数
      fn.apply(this,args);
      // * 保留上次触发的时间
      lastTime=nowTime
    }
  }
  return _throttle;
}

 <input type="text"> <button>取消</button>
  <script src="./06-throttle-v2-第一次是否立即执行.js"></script>
  <script>
    const inputEl = document.querySelector("input");
    let count = 0;
    const inputChange = function (e) {
      // * 这个this.应该是这个元素对象
      console.log(`发送了${++count}次网络请求。`, this, e);
      return "aaaaa"
    }

    inputEl.oninput = throttle(inputChange, 3000, { leading: false, trailing: false })

  </script>

2.5 节流函数v3-最后一次执行

function throttle(fn,interval,options={leading:true,trailing:false}){
  // * 记录上一次的开始时间
  let lastTime=0;
  // * 将是否第一次触发和最后一次触发取出来
  const {leading,trailing}=options;

  // * 最后一次执行的定时器
  let timer=null;

  // * 事件触发时,真正执行的函数
  const _throttle=function(...args){
    // * 获取当前事件触发时的时间  getTime 获取的是时间戳
    const nowTime=new Date().getTime();
    // * 第一次不触发的时候,将lastTime设置为nowTime
   if(!lastTime && !leading) lastTime=nowTime;
    // * 使用当前触发的时间和上一次的开始时间、时间间隔,计算出还剩多长时间触发函数。
    const remainTime=interval-(nowTime-lastTime);
    if(remainTime<=0){
      // * 真正触发函数
      fn.apply(this,args);
      // * 保留上次触发的时间
      lastTime=nowTime;

      // * 清空timer
      if(timer){
        clearTimeout(timer);
        timer=null;
        return;
      }
    }
    if(trailing&&!timer){ //* 最后一次执行
      timer=setTimeout(()=>{
        timer=null;
        lastTime=!leading?0:new Date().getTime()
        fn.apply(this,args)
      },remainTime)
    }
  }
  return _throttle;
}

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <input type="text"> <button>取消</button>
  <!-- <script src="./05-throttlev1-基本实现.js"></script> -->
  <!-- <script src="./06-throttle-v2-第一次是否立即执行.js"></script> -->

  <script src="./07-throttle-v3-trailing.js"></script>
  <script>
    const inputEl = document.querySelector("input");
    let count = 0;
    const inputChange = function (e) {
      // * 这个this.应该是这个元素对象
      console.log(`发送了${++count}次网络请求。`, this, e);
      return "aaaaa"
    }

    inputEl.oninput = throttle(inputChange, 2000, { leading: false, trailing: true })

  </script>
</body>

</html>

2.6 节流函数v4-取消功能的实现

function throttle(fn,interval,options={leading:true,trailing:false}){
  // * 记录上一次的开始时间
  let lastTime=0;
  // * 将是否第一次触发和最后一次触发取出来
  const {leading,trailing}=options;

  // * 最后一次执行的定时器
  let timer=null;

  // * 事件触发时,真正执行的函数
  const _throttle=function(...args){
    // * 获取当前事件触发时的时间  getTime 获取的是时间戳
    const nowTime=new Date().getTime();
    // * 第一次不触发的时候,将lastTime设置为nowTime
   if(!lastTime && !leading) lastTime=nowTime;
    // * 使用当前触发的时间和上一次的开始时间、时间间隔,计算出还剩多长时间触发函数。
    const remainTime=interval-(nowTime-lastTime);
    if(remainTime<=0){
      // * 真正触发函数
      fn.apply(this,args);
      // * 保留上次触发的时间
      lastTime=nowTime;

      // * 清空timer
      if(timer){
        clearTimeout(timer);
        timer=null;
        return;
      }
    }
    if(trailing&&!timer){ //* 最后一次执行
      timer=setTimeout(()=>{
        timer=null;
        lastTime=!leading?0:new Date().getTime()
        fn.apply(this,args)
      },remainTime)
    }
  }

  _throttle.cancel=function(){
    if(timer) clearTimeout(timer)
    timer=null;
    lastTime=0;
  }

  return _throttle;
}

 <input type="text"> <button>取消</button>

  <script src="./07-throttle-v3-trailing.js"></script>
  <script>
    const inputEl = document.querySelector("input");
    let count = 0;
    const inputChange = function (e) {
      // * 这个this.应该是这个元素对象
      console.log(`发送了${++count}次网络请求。`, this, e);
      return "aaaaa"
    }

    const _throttle = throttle(inputChange, 2000, { leading: false, trailing: true })
    inputEl.oninput = _throttle;

    // 取消功能
    const cancelBtn = document.querySelector("button")
    console.log("cancelBtn:", cancelBtn);
    cancelBtn.onclick = function () {
      console.log("我来取消了");
      _throttle.cancel()
    }
  </script>

2.7 节流函数v5-返回值

  • 回调函数
  • promise对象
<input type="text"> <button>取消</button>
  <!-- <script src="./05-throttlev1-基本实现.js"></script> -->
  <!-- <script src="./06-throttle-v2-第一次是否立即执行.js"></script> -->

  <!-- <script src="./07-throttle-v3-trailing.js"></script> -->
  <script src="./08-throttle-v3-返回值.js"></script>
  <script>
    const inputEl = document.querySelector("input");
    let count = 0;
    const inputChange = function (e) {
      // * 这个this.应该是这个元素对象
      console.log(`发送了${++count}次网络请求。`, this, e);
      return "aaaaa"
    }

    const _throttle = throttle(inputChange, 2000, {
      leading: false,
      trailing: true,
      resultCallback: res => {
        console.log("真实函数的返回值:", res);
      }
    })
    const temp = function (...args) {
      _throttle.apply(this, args).then(res => {
        console.log("Promise返回的值:", res);
      })
    }
    inputEl.oninput = temp;

    // 取消功能
    const cancelBtn = document.querySelector("button")
    console.log("cancelBtn:", cancelBtn);
    cancelBtn.onclick = function () {
      console.log("我来取消了");
      _throttle.cancel()
    }
  </script>
function throttle(fn,interval,options={leading:true,trailing:false}){
  // * 记录上一次的开始时间
  let lastTime=0;
  // * 将是否第一次触发和最后一次触发取出来
  const {leading,trailing,resultCallback}=options;

  // * 最后一次执行的定时器
  let timer=null;

  // * 事件触发时,真正执行的函数
  const _throttle=function(...args){
     return new Promise((resolve,reject)=>{
        // * 获取当前事件触发时的时间  getTime 获取的是时间戳
    const nowTime=new Date().getTime();
    // * 第一次不触发的时候,将lastTime设置为nowTime
   if(!lastTime && !leading) lastTime=nowTime;
    // * 使用当前触发的时间和上一次的开始时间、时间间隔,计算出还剩多长时间触发函数。
    const remainTime=interval-(nowTime-lastTime);
    if(remainTime<=0){
      // * 真正触发函数
      const result=fn.apply(this,args);
      if(resultCallback) resultCallback(result)
      resolve(result);
      // * 保留上次触发的时间
      lastTime=nowTime;

      // * 清空timer
      if(timer){
        clearTimeout(timer);
        timer=null;
        return;
      } 
    }
    if(trailing&&!timer){ //* 最后一次执行
      timer=setTimeout(()=>{
        timer=null;
        lastTime=!leading?0:new Date().getTime()
        const result=fn.apply(this,args)
        if(resultCallback) resultCallback(result)
        resolve(result);
      },remainTime)
    }
     })
  }

  _throttle.cancel=function(){
    if(timer) clearTimeout(timer)
    timer=null;
    lastTime=0;
  }

  return _throttle;
}

3.总结

防抖和节流函数.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容