JavaScript手写防抖节流(含升级版)

239 阅读7分钟

一、防抖

1.1 认识防抖

  • 什么是防抖?
    • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间

    • 当事件密集触发时,函数的触发会被频繁的推迟

    • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数

    防抖.png
  • 应用场景
    • 输入框中频繁的输入内容,搜索或者提交信息
    • 频繁的点击按钮,触发某个事件
    • 监听浏览器滚动事件,完成某些特定操作
    • 用户缩放浏览器的resize事件

1.2 基本实现

function debounce(fn, delay) {
  // 1.用于记录上一次触发的timer
  let timer = null;

  // 2.触发事件时执行的函数
  const _debounce = () => {
    // 2.1.如果有再次触发(更多次触发事件),取消上一次的事件
    if (timer) clearTimeout(timer);

    // 2.2.延迟去执行对应的fn函数(传入的回调)
    timer = setTimeout(() => {
      fn();
      timer = null; // 执行过函数之后将timer重新置为null
    }, delay);
  };

  //返回一个新的函数
  return _debounce;
}

1.3 this绑定,参数

箭头函数不绑定this,这里不使用箭头函数

function debounce(fn, delay) {
  // 1.用于记录上一次触发的timer
  let timer = null;

  // 2.触发事件时执行的函数
  const _debounce = function (...args) {
    // 2.1.如果有再次触发(更多次触发事件),取消上一次的事件
    if (timer) clearTimeout(timer);

    // 2.2.延迟去执行对应的fn函数(传入的回调)
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null; // 执行过函数之后将timer重新置为null
    }, delay);
  };

  //返回一个新的函数
  return _debounce;
}

1.4 取消功能

能够取消函数的执行,取消时调用函数cancel即可

function debounce(fn, delay) {
  // 1.用于记录上一次触发的timer
  let timer = null;

  // 2.触发事件时执行的函数
  const _debounce = function (...args) {
    // 2.1.如果有再次触发(更多次触发事件),取消上一次的事件
    if (timer) clearTimeout(timer);

    // 2.2.延迟去执行对应的fn函数(传入的回调)
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null; // 执行过函数之后将timer重新置为null
    }, delay);
  };

  // 3.给_debounce绑定一个取消的函数
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null
  }

  //返回一个新的函数
  return _debounce;
}

1.5 立即执行功能

给函数加一个参数默认赋值为false,使用时可传入第三个参数;在函数内部尽量不修改传入的变量

原则:一个函数进行做一件事情, 一个变量也用于记录一种状态

function debounce(fn, delay, immediate = false) {
    // 1.用于记录上一次触发的timer
    let timer = null;
    // 定义是否执行函数的变量
    let isInvoke = false;

    // 2.触发事件时执行的函数
    const _debounce = function (...args) {
      // 2.1.如果有再次触发(更多次触发事件),取消上一次的事件
      if (timer) clearTimeout(timer);

      // 第一次执行时不需要延迟
      if (immediate && !isInvoke) {
        fn.apply(this, args);
        isInvoke = true;
        return;
      }

      // 2.2.延迟去执行对应的fn函数(传入的回调)
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null; // 执行过函数之后将timer重新置为null
        isInvoke = false;
      }, delay);
    };

    // 3.给_debounce绑定一个取消的函数
    _debounce.cancel = function () {
      if (timer) clearTimeout(timer);
      timer = null;
      isInvoke = false;
    };

    //返回一个新的函数
    return _debounce;
  }

1.6 获取返回值

针对普通函数使用(非绑定事件),可能会有返回值,这时就需要有返回值处理

因为延时执行,有异步操作,所以结果处理有以下两种

  • 回调函数版

    function debounce(fn, delay, immediate = false, resultCallback) {
        // 1.用于记录上一次触发的timer
        let timer = null;
        let isInvoke = false;
    
        // 2.触发事件时执行的函数
        const _debounce = function (...args) {
          // 2.1.如果有再次触发(更多次触发事件),取消上一次的事件
          if (timer) clearTimeout(timer);
    
          // 第一次执行时不需要延迟
          let res;
          if (immediate && !isInvoke) {
            res = fn.apply(this, args);
            if (resultCallback) resultCallback(res);
            isInvoke = true;
            return;
          }
    
          // 2.2.延迟去执行对应的fn函数(传入的回调)
          timer = setTimeout(() => {
            res = fn.apply(this, args);
            if (resultCallback) resultCallback(res);
            timer = null; // 执行过函数之后将timer重新置为null
            isInvoke = false;
          }, delay);
        };
    
        // 3.给_debounce绑定一个取消的函数
        _debounce.cancel = function () {
          if (timer) clearTimeout(timer);
          timer = null;
          isInvoke = false;
        };
    
        //返回一个新的函数
        return _debounce;
      }
    
    • 使用
    const debounceFn = debounce(
        function (name, age, height) {
          console.log("----------", name, age, height);
          return "aaaa";
        },
        1000,
        false,
        function (res) {
          console.log("执行后的结果", res); // aaaa
        }
      );
    
      debounceFn("李雷", 18, 1.8);
    
  • promise 优雅版

    function debounce(fn, delay, immediate = false) {
      // 1.用于记录上一次触发的timer
      let timer = null;
      let isInvoke = false;
    
      // 2.触发事件时执行的函数
      const _debounce = function (...args) {
        return new Promise((resolve, reject) => {
          try {
            // 2.1.如果有再次触发(更多次触发事件),取消上一次的事件
            if (timer) clearTimeout(timer);
    
            // 第一次执行时不需要延迟
            let res;
            if (immediate && !isInvoke) {
              res = fn.apply(this, args);
              resolve(res);
              isInvoke = true;
              return;
            }
    
            // 2.2.延迟去执行对应的fn函数(传入的回调)
            timer = setTimeout(() => {
              res = fn.apply(this, args);
              resolve(res);
              timer = null; // 执行过函数之后将timer重新置为null
              isInvoke = false;
            }, delay);
          } catch (error) {
            // 处理异常
            reject(error);
          }
        });
      };
    
      // 3.给_debounce绑定一个取消的函数
      _debounce.cancel = function () {
        if (timer) clearTimeout(timer);
        timer = null;
        isInvoke = false;
      };
    
      //返回一个新的函数
      return _debounce;
    }
    
    • 使用
    const debounceFn = debounce(
        function (name, age, height) {
          console.log("----------", name, age, height);
          return "aaaa";
        },
        1000,
        false
      );
    
      debounceFn("李雷", 18, 1.82).then((res) => {
        console.log("拿到执行结果:", res); // aaaa
      });
    

二、节流

2.1 认识节流

  • 什么是节流?

    • 当事件触发时,会执行这个事件的响应函数
    • 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数
    • 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的

    节流.png

  • 应用场景

    • 监听页面的滚动事件
    • 鼠标移动事件
    • 用户频繁点击按钮操作
    • 游戏中的一些设计

2.2 基本实现

  • 使用定时器实现
    function throttle(fn, interval) {
      let timer = null;
    
      const _throttle = function (...args) {
        if (timer) return;
    
        timer = setTimeout(() => {
          fn.apply(this, args);
          timer = null;
        }, interval);
      };
    
      return _throttle;
    }
    
  • 计算时间
    • waitTime = interval - (nowTime - startTime)
    • 等待时间 = 间隔时间 - (当前时间 - 开始时间)
    • 如果 waitTime 小于等于0,就立马执行函数,需要将startTime更改为当前时间 nowTime
    function throttle(fn, interval) {
      // 1.记录上一次开始时间
      let startTime = 0;
    
      // 2.事件触发时,真正执行的函数
      const _throttle = function () {
        // 2.1.获取当前事件触发的时间
        const nowTime = new Date().getTime();
    
        // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还有剩余多长的时间去触发函数
        const waitTime = interval - (nowTime - startTime);
    
        if (waitTime <= 0) {
          // 2.3.执行函数
          fn();
          // 2.4.保留上次触发时间
          startTime = nowTime;
        }
      };
    
      return _throttle;
    }
    

2.3 this绑定,参数

function throttle(fn, interval) {
  // 1.记录上一次开始时间
  let startTime = 0;

  // 2.事件触发时,真正执行的函数
  const _throttle = function (...args) {
    // 2.1.获取当前事件触发的时间
    const nowTime = new Date().getTime();

    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还有剩余多长的时间去触发函数
    const waitTime = interval - (nowTime - startTime);

    if (waitTime <= 0) {
      // 2.3.执行函数
      fn.apply(this, args);
      // 2.4.保留上次触发时间
      startTime = nowTime;
    }
  };

  return _throttle;
}

2.4 立即执行功能

节流函数默认第一次是立即执行的

function throttle(fn, interval, leading = true) {
  // 1.记录上一次开始时间
  let startTime = 0;

  // 2.事件触发时,真正执行的函数
  const _throttle = function (...args) {
    // 2.1.获取当前事件触发的时间
    const nowTime = new Date().getTime();

    // 对立即执行进行控制(仅在第一次时控制)
    if (!leading && startTime === 0) {
      startTime = nowTime;
    }

    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还有剩余多长的时间去触发函数
    const waitTime = interval - (nowTime - startTime);

    if (waitTime <= 0) {
      // 2.3.执行函数
      fn.apply(this, args);
      // 2.4.保留上次触发时间
      startTime = nowTime;
    }
  };

  return _throttle;
}

2.5 尾部执行

主要用来判断最后一次的等待时间未达到节流控制的时间是否执行函数

function throttle(
  fn,
  interval,
  { leading = true, trailing = false } = {}
) {
  // 1.记录上一次开始时间
  let startTime = 0;
  let timer = null;

  // 2.事件触发时,真正执行的函数
  const _throttle = function (...args) {
    // 2.1.获取当前事件触发的时间
    const nowTime = new Date().getTime();

    // 对立即执行进行控制
    if (!leading && startTime === 0) {
      startTime = nowTime;
    }

    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还有剩余多长的时间去触发函数
    const waitTime = interval - (nowTime - startTime);

    if (waitTime <= 0) {
      if (timer) clearTimeout(timer);

      fn.apply(this, args);
      startTime = nowTime;
      timer = null;
      // 刚刚好在此时执行,就不需要尾部处理了
      return;
    }

    // 判断是否执行尾部
    if (trailing && !timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        startTime = new Date().getTime();
        timer = null;
      }, waitTime);
    }
  };

  return _throttle;
}

2.6 取消功能

function throttle(
  fn,
  interval,
  { leading = true, trailing = false } = {}
) {
  // 1.记录上一次开始时间
  let startTime = 0;
  let timer = null;

  // 2.事件触发时,真正执行的函数
  const _throttle = function (...args) {
    // 2.1.获取当前事件触发的时间
    const nowTime = new Date().getTime();

    // 对立即执行进行控制
    if (!leading && startTime === 0) {
      startTime = nowTime;
    }

    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还有剩余多长的时间去触发函数
    const waitTime = interval - (nowTime - startTime);

    if (waitTime <= 0) {
      if (timer) clearTimeout(timer);

      fn.apply(this, args);
      startTime = nowTime;
      timer = null;
      // 刚刚好在此时执行,就不需要尾部处理了
      return;
    }

    // 判断是否执行尾部
    if (trailing && !timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        startTime = new Date().getTime();
        timer = null;
      }, waitTime);
    }
  };

  // 取消功能
  _throttle.cancel = function () {
    if (timer) clearTimeout(timer);
    startTime = 0;
    timer = null;
  };

  return _throttle;
}

2.7 获取返回值

function throttle(
  fn,
  interval,
  { leading = true, trailing = false } = {}
) {
  // 1.记录上一次开始时间
  let startTime = 0;
  let timer = null;

  // 2.事件触发时,真正执行的函数
  const _throttle = function (...args) {
    return new Promise((resolve, reject) => {
      try {
        // 2.1.获取当前事件触发的时间
        const nowTime = new Date().getTime();
        // 返回结果
        let res;

        // 对立即执行进行控制
        if (!leading && startTime === 0) {
          startTime = nowTime;
        }

        // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还有剩余多长的时间去触发函数
        const waitTime = interval - (nowTime - startTime);

        if (waitTime <= 0) {
          if (timer) clearTimeout(timer);

          res = fn.apply(this, args);
          resolve(res);
          startTime = nowTime;
          timer = null;
          // 刚刚好在此时执行,就不需要尾部处理了
          return;
        }

        // 判断是否执行尾部
        if (trailing && !timer) {
          timer = setTimeout(() => {
            res = fn.apply(this, args);
            resolve(res);
            startTime = new Date().getTime();
            timer = null;
          }, waitTime);
        }
      } catch (error) {
        reject(error);
      }
    });
  };

  // 取消功能
  _throttle.cancel = function () {
    if (timer) clearTimeout(timer);
    startTime = 0;
    timer = null;
  };

  return _throttle;
}