# 节流防抖

# 1、debounce 防抖

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

  • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
  • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次。
  • 实现方式:使用setTimeout实现,再触发的时候清除定时器, 重新计时

防抖是一段时候后触发, 如果时间段内再次触发则重新计时。触发延后。因为防抖要的是最后的那个状态, 比如输入的结果是最后的状态, resize也是需要根据最后不动的宽高来决定其他元素的布局。

# 2、throttle 节流

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

  • 鼠标不断点击触发,mousedown(单位时间内只触发一次),防止触发多次事件
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
  • 实现方式:可以使用一个变量存储事件是否允许执行,如果允许则执行函数并且把该变量修改为不允许,并使用定时器在规定时间后重置变量为允许,或者等异步操作结果返回再重置变量为允许。

节流是一开始就会触发一次, 一定时间内如果再次触发无效。 比如滚动到底部加载更多, 滚到那个距离就直接触发, 后面继续滚动不再触发。

# 3、防抖函数实现

# 3.1 利用setTimeout实现一个简易的防抖函数

function debounce(fn, delay = 0) {
  let _timer = 0;
  return function (...args) {
    if (_timer) {
      clearTimeout(_timer);
    }
    _timer = setTimeout(() => {
      try {
        fn(...args);
      } catch (error) {
        throw error;
      }
      _timer = 0;
    }, delay);
  };
}

const foo = debounce(a => {
  console.log(a);
}, 100);

foo(1);
foo(2);
foo(3);
// 最后只会输出3

# 3.2 在上述的基础之上关注this,新增immediate 参数

function debounce(fn, delay = 0, immediate = false) {
  let timer = 0,
    result;
  let _immediate = immediate;
  return function (...args) {
    let context = this;
    if (_immediate && !timer) {
      _immediate = false;
      result = fn.apply(context, args);
    }
    if (timer) {
      clearTimeout(timer);
      timer = 0;
    }
    timer = setTimeout(() => {
      timer = 0;
      fn.apply(context, args);
    }, delay);
    return result;
  };
}

const obj = { name: 'obj' };

obj.print = debounce(
  function (a, b) {
    console.log(a, b, this);
    return a + b;
  },
  100,
  true
);

obj.print(1, 1);
obj.print(2, 2);
obj.print(3, 3);

immediate 设置为true会在初次调用时先执行一次, 这时候尽量返回回调函数的返回值。再利用apply实现this绑定。

# 3.3 使用 requestAnimationFrame 代替setTimeout

// delayCallback 的delay参数传入0的话, 回调函数会被同步执行
function delayCallback(callback, delay = 0) {
  let start = Date.now();
  let requestId;
  function fn() {
    let end = Date.now();
    if (end - start >= delay) {
      callback();
    } else {
      requestId = requestAnimationFrame(fn);
      // window.cancelAnimationFrame(requestId)
    }
    return requestId;
  }
  return fn();
}
function debounce(fn, delay = 0, immediate = false) {
  let timer = 0;
  let _immediate = immediate;
  return function (...args) {
    let context = this;
    if (_immediate && !timer) {
      _immediate = false;
      fn.apply(context, args);
    }
    if (timer) {
      window.cancelAnimationFrame(timer);
      timer = 0;
    }
    timer = delayCallback(() => {
      timer = 0;
      fn.apply(context, args);
    }, delay);
  };
}

# 3.4 setTimeout和requestAnimationFrame兼容使用

function delayCallback(callback, delay) {
  let start = Date.now();
  let requestId;
  function fn() {
    let end = Date.now();
    if (end - start >= delay) {
      callback();
    } else {
      requestId = requestAnimationFrame(fn);
      // window.cancelAnimationFrame(requestId)
    }
    return requestId;
  }
  return fn();
}

function debounce(fn, delay = 0, immediate = false) {
  let timer = 0;
  let _immediate = immediate;

  const useRAF = delay !== 0 && typeof window.requestAnimationFrame === 'function';

  return function (...args) {
    let context = this;
    if (_immediate && !timer) {
      _immediate = false;
      fn.apply(context, args);
    }
    if (timer) {
      //
      if (useRAF) {
        window.cancelAnimationFrame(timer);
      } else {
        clearTimeout(timer);
      }

      timer = 0;
    }
    let nextTick;
    if (useRAF) {
      nextTick = delayCallback;
    } else {
      nextTick = setTimeout;
    }
    timer = nextTick(() => {
      timer = 0;
      fn.apply(context, args);
    }, delay);
  };
}

其实大多数情况下, 第二种方式就足够使用了

# 3.5 实现cancel取消功能,取消防抖设定

function debounce(fn, delay = 0, immediate = false) {
  let timer = 0,
    result,
    disabled = false,
    _immediate = immediate;
  const _debounce = function (...args) {
    let context = this;
    if (disabled) {
      return fn.apply(context, args);
    }
    if (_immediate && !timer) {
      _immediate = false;
      result = fn.apply(context, args);
    }
    if (timer) {
      clearTimeout(timer);
      timer = 0;
    }
    timer = setTimeout(() => {
      timer = 0;
      fn.apply(context, args);
    }, delay);
    return result;
  };
  //   取消防抖
  _debounce.cancel = function () {
    clearTimeout(timer);
    timer = 0;
    disabled = true;
  };
  return _debounce;
}

用法举例

const useMove = debounce(e => {
  console.log(e.clientX);
}, 100);
document.addEventListener('mousemove', useMove);
setTimeout(() => {
  useMove.cancel();
}, 2000);

# 4、节流函数实现

# 4.1 使用setTimeout实现简易的节流函数

function throttle(fn, delay = 0) {
  let timer = 0;
  let flag = true;
  let result;
  return function (...args) {
    const context = this;
    if (flag) {
      result = fn.apply(context, args);
      flag = false;
    } else {
      result = undefined;
    }
    timer = setTimeout(() => {
      flag = true;
    }, delay);
    return result;
  };
}

其他大部分时候的异步开销节流, 可以自己定义一个变量来控制。

# 4.2 使用时间值来判断

function throttle(fn, delay = 0) {
  let startTime = 0;
  let result;
  return function (...args) {
    const context = this;
    const now = Date.now();
    if (now - startTime >= delay) {
      result = fn.apply(context, args);
      startTime = now;
    } else {
      result = undefined;
    }
    return result;
  };
}

用法举例

const foo = throttle(function (a) {
  console.log(a);
  return a;
}, 100);
foo(1);
foo(2);
setTimeout(() => {
  foo(3);
}, 200);
上次更新: 1/22/2025, 9:39:13 AM