14.防抖

94 阅读4分钟

实现

function debounce(func, wait, immediate) { // 形参赋值默认值,会引起麻烦机制,所以不推荐
  // 多个参数及传递和不传递的默认处理
  if (typeof func !== 'function') throw new TypeError('func must be a function');
  if (typeof wait === 'undefined') wait = 500;
  if (typeof wait === 'boolean') {
    immediate = wait
    wait = 500
  }
  if (typeof immediate !== 'boolean') immediate = false;

  // 设定定时器返回标识
  let timer = null
  return function proxy(...params) {
    let self = this // 保证原始函数的形参和this执行

    if (immediate) {
      // 是否设置过定时器标识(设置后把定时器标识timer为数字),来截断函数执行,
      // 是:退出,不做任何处理
      if (timer) return;
      // 否:没有设置过,则立即执行,且设置定时器标识截断
      func.call(self, ...params)
      timer = setTimeout(function () {
        timer = null // 把定时器标识重置为null
      }, wait)
    }
    // 否:一般适用于‘输入模糊搜索’事件,需要输入完成后,最后一次响应
    else {
      clearTimeout(timer)
      timer = setTimeout(function () {
        func.call(self, ...params)
      }, wait)
    }
  }
}

分析过程

/**
 * 函数得防抖(debounce)和节流(throttle)
 *  在”高频“触发的场景下,需要进行防抖和节流
 *    + 狂点一个按钮(开头触发)
 *    + 页面滚动
 *    + 输入模糊匹配(最后一次触发)
 *    + ...
 * 我们自己设定,多长的时间内,触发两次即以上就算“高频”:封装方法的时候需要指定这个频率(可以设置默认值)
 * 「防抖」在某一次高频触发下,我们只识别一次(可以控制开始触发,还是最后一次触发);详细:假设我们规定500ms触发多次算是高频,只要们检测到是高频触发了,则在本次频繁操作下(哪怕你操作了10min)也只触发一次
 *    可以开始,也可以结尾触发,一般按钮点击倾向于开始节点处触发,这样没有延迟
 * 「节流」在某一次高频触发下,我们不是只识别一次,按照我们设定的间隔时间(自己规定的频率),没有达到这个频率都会触发一次;详细:假如我们规定频率是500ms,我们操作了10min,触发的次数=(10*60*1000)/500
 */
const submit = document.querySelector('.submit')
console.log('submit---:', submit)
// submit.onclick = function () {
//   console.log('ok')
// }
// widow.onscroll = function () {
//   // 默认情况下:浏览器在最快的反应时间内(4~6ms),就会识别监听一次事件触发,把绑定的方法执行,这样导致方法执行的次数过多,造成不必要的资源浪费
//   console.log('ok')
// }

// // 业务场景中处理的技巧1:标识判断
// let flag = false
// submit.onclick = function () {
//   if (flag) return;
//   flag = true
//   console.log('ok')
//   setTimeout(function () {
//     // 事情处理完
//     flag = false
//   }, 1000)
// }

// // 业务场景中处理技巧2:按钮置灰,移除事件绑定
// function handle() {
//   submit.onclick = null
//   submit.disabled = true
//   // ...
//   console.log('ok')
//   setTimeout(function () {
//     submit.onclick = handle
//     submit.disabled = false
//   }, 1000)
// }
// submit.onclick = handle;


// lodash 实现
function debounce(func, wait, immediate) { // 形参赋值默认值,会引起麻烦机制,所以不推荐
  // 多个参数及传递和不传递的默认处理
  if (typeof func !== 'function') throw new TypeError('func must be a function');
  if (typeof wait === 'undefined') wait = 500;
  if (typeof wait === 'boolean') {
    immediate = wait
    wait = 500
  }
  if (typeof immediate !== 'boolean') immediate = false;

  // 设定定时器返回标识
  let timer = null
  return function proxy(...params) {
    let self = this // 保证原始函数的形参和this执行

    // // 方式一:混合兼容
    // let now = immediate && !timer;
    // clearTimeout(timer)
    // timer = setTimeout(function () {
    //   timer = null
    //   !immediate ? func.call(self, ...params) : null
    // }, wait)
    // // 第一次触发就立即执行
    // now ? func.call(self, ...params) : null

    // 方式二: 拆分immediate处理情况
    // 是否立即执行
    // 是:一般适用于‘按钮点击’事件,需要点击立即响应
    if (immediate) {
      // 是否设置过定时器标识(设置后把定时器标识timer为数字),来截断函数执行,
      // 是:退出,不做任何处理
      if (timer) return;
      // 否:没有设置过,则立即执行,且设置定时器标识截断
      func.call(self, ...params)
      timer = setTimeout(function () {
        timer = null // 把定时器标识重置为null
      }, wait)
    }
    // 否:一般适用于‘输入模糊搜索’事件,需要输入完成后,最后一次响应
    else {
      clearTimeout(timer)
      timer = setTimeout(function () {
        func.call(self, ...params)
      }, wait)
    }
  }

}
function handle() {
  // 具体在点击的时候要处理的业务
  console.log('ok')
}
submit.onclick = debounce(handle, true)
// submit.onclick=proxy; 疯狂点击的情况下,proxy会被疯狂执行,我们需要在proxy中根据频率管控handle的执行次数「函数柯里化」
// submit.onclick = handle; // handle-this:submit 传递一个事件对象



/**
 * 过程分析
 * 定时器目的,检测500ms内是否会触发第二次,如果有,则为高频触发
 * 第一次proxy执行,设置一个定时器
 * 4ms
 * 第二次proxy执行,清除之前设定的,重新设定,再去检测500ms内是否有第二次触发
 * ...
 * 疯狂点击100次
 * 之前99次定时器都清除了
 * 只留最后一个
 * 等到过了500ms 发现没有触发第二次 执行函数
 */