将防抖节流运用于实际场景中

目录

  1. 防抖
  2. 节流
  3. 应用场景
  4. 案例(含特例)
  5. 总结
  6. 拓展:执行顺序

一、防抖

对于短时间内连续触发的事件(如:滚动事件),防抖的含义就是让某个时间期限(如 1000 毫秒)内,事件处理函数只执行一次
思路:在第一次触发事件时,不立即执行函数,而是给出一个期限值(delay)比如 200ms,然后:

  • 如果在 delay 内没有再次触发事件,那么就执行函数。
  • 如果在 delay 内再次触发事件,那么当前的计时取消,重新开始计时。

效果:如果短时间内大量触发同一事件,只会执行一次函数。

实现:既然前面都提到了计时,那实现的关键就在于setTimeout这个函数,由于还需要一个变量来保存计时,考虑维护全局纯净,可以借助闭包来实现。

// 以滚动事件为例子
function debounce(fn,delay){
  let timer = null // 借助闭包
  return function() {
    if(timer){
      clearTimeout(timer) 
    }
    timer = setTimeout(fn,delay) // 简化写法
  }
}
// 重写事件
function showTop  () {
  var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滚动条位置:' + scrollTop);
}
window.onscroll = debounce(showTop,200) // 实际使用根据需要来配置间断值

出现问题:如果在限定时间段内,不断触发滚动事件(比如某个用户闲着无聊,按住滚动不断的拖来拖去),只要不停止触发,理论上就永远不会输出当前距离顶部的距离。

要求:即使用户不断拖动滚动条,也能在某个时间间隔之后给出反馈,这个时候可以考虑节流。

二、节流

思路:设置类似控制阀门一样定期开放的函数,也就是让函数执行一次后,在某个时间段内暂时失效,过了这段时间后再重新激活(类似于技能冷却时间),也利用了 setTimeout 的异步执行机制。

效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。

实现

方式一:【定时器】借助 setTimeout 来实现,利用状态位 valid 来表示当前函数是否处于工作状态。

function throttle(fn,delay){
  let valid = true
  return function() {
    if(valid){
      // 利用异步执行的原理
      setTimeout(() => {
        fn()
        valid = true;
      }, delay)	
    }
    // 工作时间,执行函数并且在间隔期内把状态位设为无效
    valid = false
  }
}
// 重写事件
function showTop  () {
  var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
 console.log('滚动条位置:' + scrollTop);
}
window.onscroll = throttle(showTop,1000) 

方式二:【时间戳】利用时间戳差值是否大于指定间隔时间来做判定

function throttle(fn, delay) {
  let prev = Date.now()
  return function () {
    let now = Date.now()
    if (now - prev >= delay) {
      fn()
      prev = Date.now()
    }
  }
}

方式三:【定时器 + 时间戳】在节流函数内部使用开始时间 startTime、当前时间 curTime 与 delay 来计算剩余时间 remaining,以作为标志来判断。

function throttle(fn, delay) {
  let timer = null
  let startTime = Date.now()
  return function () {
    let curTime = Date.now()
    let remaining = delay - (curTime - startTime)
    clearTimeout(timer)
    if (remaining <= 0) {
      fn()
      startTime = Date.now()
    } else {
      timer = setTimeout(fn, remaining)
    }
  }
}

三、应用场景

防抖(debounce):只执行最后一次

  • 搜索框搜索输入,在用户在不断输入值时,如果只需用户最后一次输入完再发送请求,可以用防抖来节约请求资源。
  • 手机号、邮箱验证输入检测 (change、input、blur、keyup 等事件触发,每次键入都会触发)的场景。
  • window 触发 resize 的时候,不断的调整浏览器窗口大小会不断的触发这个事件,但实际只需窗口调整完成后再计算窗口大小,为防止重复渲染,可以用防抖来让其只触发一次。
  • 鼠标的 mousemove、mouseover 事件。
  • 网页导航条的多次点击的情况,只需要最后一次跳转。
  • 等等

节流(throttle):对那些耗性能的高频操作减少执行次数

  • 搜索框搜索联想的功能。
  • 鼠标不断点击触发的事件,比如轮播图的切换。
  • 监听滚动事件 onscroll,比如是否滑到底部自动加载更多,用 throttle 来判断。
  • 提交按钮,对于用户高频点击提交的情况,可以防止表单重复提交。
  • DOM 元素的拖拽功能实现(mousemove)
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • Canvas 模拟画板功能(mousemove)
  • 计算鼠标移动的距离(mousemove)
  • 等等

四、案例(含特例)

1 防抖:输入案例

  • 输入框input在输入过程中会多次触发,但是我们只要最后一次 —— 防抖。
  • 注意this的指向,因为内部函数最终是input调用的,因此箭头函数内的this是指向input,但是fn函数传过来的this是指向window的,因此需要解决(下面两种方法均可)
<input type='text' />
<script>
  var input = document.querySelector('input');
  /* 如果不加防抖:在输入过程中会一直触发
   * input.oninput = function () {
   *   console.log(input.value);
   */ }
  // 解决方法:防抖 ----> 利用setTimeout的标识timer来判断是否开启的定时器
  function debounce(fn, delay) {
    let timer = null; // 闭包
    this.a = 1;
    return function () {
      if (timer != null) {
        clearTimeout(timer); // 标识作为参数传递
      }
      this.a = 2;
      timer = setTimeout(() => {
        // 箭头函数: <input type='text' /> '1' 此时可以该匿名函数内的this参数
        // 非箭头函数:Window {window: Window, self: Window, document: document, name: '', location: Location, …} '1'
        console.log(this.a, "1");  // 2
        // 但是对于外部传进来函数的的this,指的还是window
        fn();  // Window {window: Window, self: Window, document: document, name: '', location: Location, …} '2'
      }, delay)
    }
  }
  input.oninput = debounce(function () { console.log(this, "2"); }, 1000);
  // 改成  写法1
  function debounce(fn, delay) {
    let timer = null; // 闭包
    return function () {
      if (timer != null) {
        clearTimeout(timer); // 标识作为参数传递
      }
      timer = setTimeout(() => { // 现在使用箭头函数,setTimeout里面指的是整个return函数作用域
        // 把当前的this替换那边函数的this
        fn.call(this);
        // 或者 fn.bind(this)();
      }, delay)
    }
  }
  input.oninput = debounce(function () { console.log(this.value); }, 1000);
  // 写法2
  function debounce(fn, delay) {
    let timer = null; // 闭包
    return function () {
      if (timer != null) {
        clearTimeout(timer); // 标识作为参数传递
      }
      // 对setTimeout绑定bind(apply、call不行,他们会立即执行)
      timer = setTimeout(function () { // 箭头函数
        // 这时候setTimeout异步指向window
        // 对setTimeout绑定bind后指向input
        // console.log(this)  为<input type='text' />
        fn.call(this);   // 在用这个this替换传过来的this
      }.bind(this), delay)
    }
  }
  input.oninput = debounce(function () { console.log(this.value); }, 1000);
</script>

2 节流:滚动案例

  • 滚动条滚动过程中会多次触发,但是我们不需要太多次——节流。
<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
<script>
  // 节流:减少执行次数
  // 例如:这样设置后,1s只能执行一次了
  let flag = true;
  window.onscroll = function () {
    // 1.第一次为true肯定执行
    if (flag) {
      // 2.setTimeout是异步函数,遇到异步函数,进入event table,1s后进入任务队列
      // 4.当同步任务执行完毕,到任务队列执行该函数,执行后flag为true
      setTimeout(() => {
        console.log('123');
        flag = true;
      }, 1000)
    }
    // 3.此时flag为false,再次触发时不执行
    flag = false;
  }
  // 写法1
  function throttle(fn, delay) {
    let flag = true;
    return function () {
      if (flag) {
        setTimeout(() => {
          fn.call(this);
          flag = true;
        }, delay)
      }
      flag = false;
    }
  }
window.onscroll = throttle(function () { console.log(window.pageYOffset) }, 1000)
</script>

3 每个请求必须发送的问题

对于购买页来说,购买页改变任何一个选项,都会调用查价接口,然后显示对应的价格。尤其是购买数量,这是一个数字选择器,如果用户频繁点击“+”号,就会连续调用多次查价接口,但最后一次的查价接口返回的数据才是最后选择的正确的价格。每个查价接口逐个请求完毕的时候,显示价格也会逐个改变,最终变成最后正确的价格,一般来说,这是比较不友好的,用户点了多次后,不想看到价格在变化,尽管最终是正确的价格,但这个变化的过程是不能接受的。
现在的问题是:不能使用防抖来解决该问题,因为不能设置过长的定时器,查价接口不能等太久,也不能设置过短的定时器,否则会出现上面说的问题(价格在变化)。对于这种问题,可以采用入栈、取栈顶元素以比对请求参数的方法解决。

// 查价
async getPrice() {
  // 请求参数
  const reqData = this.handleData()
  // push 入栈
  this.priceStack.push(reqData)
  const { result } = await getProductPrice(reqData)
  // 核心代码:取栈顶元素(最后请求的参数)比对
  if(this.$lang.isEqual(this.$array.last(this.priceStack), reqData)) {
    // 展示价格代码...
  }
}

上述的 this.$lang.isEqual、this.$array.last 均是 lodash 插件提供的方法,需要注册到 Vue 中。

import array from 'lodash/array'
import Lang from 'lodash/lang'
Vue.prototype.$array = array
Vue.prototype.$lang = Lang

五、总结

1 防抖

function debouce(fn,delay) {
  // 此处的 ...arguments 是 debouce 的参数
  let timer = null;
  let args = arguments
  return function() {
    // 此处的 ...arguments 是 demo 的参数
    if(timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.call(this, ...args)
    }, delay)  
  }
}
function fn(param) {
  // 此处的 ...arguments 是 fn 的参数
  console.log(param);
}
var demo = debouce(fn,1000);

2 节流

/*  定时器
 *  1. 当第一次触发事件时,不会立即执行函数,而是在 delay 秒后才执行。
 *  2. 而后再怎么频繁触发事件,也都是每 delay 时间才执行一次。
 */ 3. 当最后一次停止触发后,由于定时器的 delay 延迟,可能还会执行一次函数。
function throttle(fn,delay) {
  // 此处的 ...arguments 是 throttle 的参数
  let flag = true;
  let args = arguments
  return function() {
    // 此处的 ...arguments 是 demo 的参数
    if(flag) {
      setTimeout(() => {
        fn.call(this, ...args);
        flag = true;
      }, delay)
    }
    flag = false;
  }
}
function fn(param) {
  // 此处的 ...arguments 是 fn 的参数
  console.log(param);
}
var demo = throttle(fn,1000);
/*  时间戳
 *  1. 再怎么频繁地触发事件,也都是每 delay 时间才执行一次。
 */ 2. 而当最后一次事件触发完毕后,事件也不会再被执行了。
function throttle(fn, delay) {
  let prev = Date.now()
  let args = arguments
  return function () {
    let now = Date.now()
    if (now - prev >= delay) {
      fn.call(this, ...args)
      prev = Date.now()
    }
  }
}
/*  时间戳 + 定时器
 *  1. 在节流函数内部使用开始时间 startTime、当前时间 curTime 与 delay 来计算剩余时间 remaining
 *  2. 当 remaining<=0 时表示该执行事件处理函数了(保证了第一次触发事件就能立即执行事件处理函数和每隔 delay 时间执行一次事件处理函数)
 *  3. 如果还没到时间的话就设定在 remaining 时间后再触发 (保证了最后一次触发事件后还能再执行一次事件处理函数)
 */ 4. 当然在 remaining 这段时间中如果又一次触发事件,那么会取消当前的计时器,并重新计算一个 remaining 来判断当前状态。
function throttle(fn, delay) {
  let timer = null
  let startTime = Date.now()
  let args = arguments
  return function () {
    let curTime = Date.now()
    let remaining = delay - (curTime - startTime)
    clearTimeout(timer)
    if (remaining <= 0) {
      fn.call(this, ...args)
      startTime = Date.now()
    } else {
      timer = setTimeout(() => {
        fn.call(this, ...args)
      }, remaining)
    }
  }
}

六、拓展:执行顺序

1 防抖 + setTimeout

function debouce(fn,delay) {
  let timer = null;
  return function() {
    if(timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.call(this, ...arguments)
    }, delay)  
  }
}
function fn(param) {
  console.log(param);
}
var demo = debouce(fn,1000);
// 题1:连续触发只执行最后一次
demo(1);
demo(2);
demo(4);
// 4
// 题2:先执行124,遇到demo(3)立即放入任务队列,而这个时候4的1s还没结束(4是异步任务),执行3后清除4
demo(1);
demo(2);
setTimeout(()=>{demo(3)})
demo(4);
// 3
// 题3:先执行124,遇到demo(3)等1s后放入任务队列,而这个时候4的1s还没结束,执行3后清除4
demo(1);
demo(2);
setTimeout(()=>{demo(3)},1000)
demo(4);
// 3
// 题4:先执行124,遇到demo(3)等1.001s后放入任务队列,而这个时候4的1s结束了,然后执行3
demo(1);
demo(2);
setTimeout(()=>{demo(3)},1001)
demo(4);
// 4 3
// 题5:先执行12,遇到demo(3)等1s后放入任务队列,而这个时候2的1s结束了,然后执行3
demo(1);
demo(2);
setTimeout(()=>{demo(3)},1000)
// 2 3

2 节流 + setTimeout

function throttle(fn,delay) {
  let flag = true;
  return function() {
    if(flag) {
      setTimeout(() => {
        fn.call(this,...arguments);
        flag = true;
      }, delay)
    }
    flag = false;
  }
}
function fn(param) {
  console.log(param);
}
var demo = throttle(fn,1000);
// 题1:第一次还没结束后面的为false
demo(1);
demo(2);
demo(3);
// 1
// 题2:先执行12,遇到demo(3)立即放入任务队列,而这个时候1的1s还没结束(1是异步任务),3不执行
demo(1);
demo(2);
setTimeout(()=>{demo(3)})
// 1
// 题3:先执行12,遇到demo(3)在1s后放入任务队列,而这个时候1的1s结束了(1是异步任务),3执行
demo(1);
demo(2);
setTimeout(()=>{demo(3)},1000)
// 1 3
// 题4:任务队列里有1的setTimeout和demo(2)、demo(3)(1s后进入任务队列)、demo(4)(1s后进入任务队列)
demo(1);
setTimeout(()=>{demo(2)})
setTimeout(()=>{demo(3)},1000)
setTimeout(()=>{demo(4)},1000)
// 1 3
posted @ 2023-03-01 20:20  琪有此理  阅读(84)  评论(0编辑  收藏  举报