手写节流防抖函数

2022-10-05 10:47:15 浏览数 (2)

1. 认识防抖和节流函数

防抖和节流的概念最早不是出现在软件工程中,防抖是出现在电子元件中,节流是出现的流体流动中。

  • 而javascript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理
  • 而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生

1.1. 认识防抖debounce函数

场景:在实际开发中,常常会碰到点击一个按钮请求网络接口的场景,这时用户如果因为手抖多点了几下按钮,就会出现短时间内多次请求接口的情况,实际上这会造成性能的消耗,我们其实只需要监听最后一次的按钮,但是我们并不知道哪一次会是最后一次,就需要做个延时触发的操作,比如这次点击之后的300毫秒内没再点击就视为最后一次。这就是防抖函数使用的场景

总结防抖函数的逻辑

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

1.2 认识节流throttle函数

场景:开发中我们会有这样的需求,在鼠标移动的时候做一些监听的逻辑比如发送网络请求,但是我们知道document.onmousemove监听鼠标移动事件触发频率是很高的,我们希望按照一定的频率触发,比如3秒请求一次。不管中间document.onmousemove监听到多少次只执行一次。这就是节流函数的使用场景

总结节流函数的逻辑

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

2. 实现防抖函数

2.1 基本实现v-1

代码语言:javascript复制
const debounceElement = document.getElementById("debounce");

const handleClick = function (e) {
  console.log("点击了一次");
};

// debounce防抖函数
function debounce(fn, delay) {
  // 定一个定时器对象,保存上一次的定时器
  let timer = null
  // 真正执行的函数
  function _debounce() {
    // 取消上一次的定时器
    if (timer) {
      clearTimeout(timer);
    }
    // 延迟执行
    timer = setTimeout(() => {
      fn()
    }, delay);
  }
  return _debounce;
}

debounceElement.onclick = debounce(handleClick, 300);

参考 前端手写面试题详细解答

2.2 this-参数v-2

上面handleClick函数有两个问题,一个是this指向的是window,但其实应该指向debounceElement,还一个是无法传递传递参数。

优化:

代码语言:javascript复制
const debounceElement = document.getElementById("debounce");

const handleClick = function (e) {
  console.log("点击了一次", e, this);
};

function debounce(fn, delay) {
  let timer = null;
  function _debounce(...args) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args) // 改变this指向 传递参数
    }, delay);
  }
  return _debounce;
}

debounceElement.onclick = debounce(handleClick, 300);

2.3 可选是否立即执行v-3

有些时候我们想点击按钮的第一次就立即执行,该怎么做呢?

优化:

代码语言:javascript复制
const debounceElement = document.getElementById("debounce");

const handleClick = function (e) {
  console.log("点击了一次", e, this);
};

// 添加一个immediate参数 选择是否立即调用
function debounce(fn, delay, immediate = false) {
  let timer = null;
  let isInvoke = false; // 是否调用过
  function _debounce(...args) {
    if (timer) {
      clearTimeout(timer);
    }

    // 如果是第一次调用 立即执行
    if (immediate && !isInvoke) {
      fn.apply(this.args);
      isInvoke = true;
    } else {
      // 如果不是第一次调用 延迟执行 执行完重置isInvoke
      timer = setTimeout(() => {
        fn.apply(this, args);
        isInvoke = false;
      }, delay);
    }
  }
  return _debounce;
}

debounceElement.onclick = debounce(handleClick, 300, true);

2.4 取消功能v-4

有些时候我们设置延迟时间很长,在这段时间内想取消之前点击按钮的事件该怎么做呢?

优化:

代码语言:javascript复制
const debounceElement = document.getElementById("debounce");
const cancelElemetnt = document.getElementById("cancel");

const handleClick = function (e) {
  console.log("点击了一次", e, this);
};

function debounce(fn, delay, immediate = false) {
  let timer = null;
  let isInvoke = false; 
  function _debounce(...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方法 用来取消定时器
  _debounce.cancel = function () {
    clearTimeout(timer);
    timer = null;
  };
  return _debounce;
}

const debonceClick = debounce(handleClick, 5000, false);
debounceElement.onclick = debonceClick;
cancelElemetnt.onclick = function () {
  console.log("取消了事件");
  debonceClick.cancel();
};

2.5 返回值v-5(最终版本)

最后一个问题,上面handleClick如果有返回值我们应该怎么接收到呢

优化:用Promise回调

代码语言:javascript复制
const debounceElement = document.getElementById("debounce");
const cancelElemetnt = document.getElementById("cancel");

const handleClick = function (e) {
  console.log("点击了一次", e, this);
  return "handleClick返回值";
};

function debounce(fn, delay, immediate = false) {
  let timer = null;
  let isInvoke = false;
  function _debounce(...args) {
    return new Promise((resolve, reject) => {
      if (timer) clearTimeout(timer);

      if (immediate && !isInvoke) {
        try {
          const result = fn.apply(this, args);
          isInvoke = true;
          resolve(result); // 正确的回调
        } catch (err) {
          reject(err); // 错误的回调
        }
      } else {
        timer = setTimeout(() => {
          try {
            const result = fn.apply(this, args); 
            isInvoke = false;
            resolve(result); // 正确的回调
          } catch (err) {
            reject(err); // 错误的回调
          }
        }, delay);
      }
    });
  }

  _debounce.cancel = function () {
    clearTimeout(timer);
    timer = null;
  };
  return _debounce;
}

const debonceClick = debounce(handleClick, 300, true);
// 创建一个debonceCallBack用于测试返回的值
const debonceCallBack = function (...args) {
  debonceClick.apply(this, args).then((res) => {
    console.log({ res });
  });
};

debounceElement.onclick = debonceCallBack;
cancelElemetnt.onclick = () => {
  console.log("取消了事件");
  debonceClick.cancel();
};

3. 实现节流函数

3.1 基本实现v-1

这里说一下最主要的逻辑,只要 这次监听鼠标移动事件处触发的时间减去上次触发的时间大于我们设置的间隔就执行想要执行的操作就行了

nowTime−lastTime>interval

nowTime:这次监听鼠标移动事件处触发的时间

lastTime:监听鼠标移动事件处触发的时间

interval:我们设置的间隔

代码语言:javascript复制
const handleMove = () => {
  console.log("监听了一次鼠标移动事件");
};

const throttle = function (fn, interval) {
  // 记录当前事件触发的时间
  let nowTime;
  // 记录上次触发的时间
  let lastTime = 0;

  // 事件触发时,真正执行的函数
  function _throttle() {
    // 获取当前触发的时间
    nowTime = new Date().getTime();
    // 当前触发时间减去上次触发时间大于设定间隔
    if (nowTime - lastTime > interval) {
      fn();
      lastTime = nowTime;
    }
  }

  return _throttle;
};

document.onmousemove = throttle(handleMove, 1000);

3.2 this-参数v-2

和防抖一样,上面的代码也会有this指向问题 以及 参数传递

优化:

代码语言:javascript复制
const handleMove = (e) => {
    console.log("监听了一次鼠标移动事件", e, this);
};

const throttle = function (fn, interval) {
  let nowTime;
  let lastTime = 0;

  function _throttle(...args) {
    nowTime = new Date().getTime();
    if (nowTime - lastTime > interval) {
      fn.apply(this, args);
      lastTime = nowTime;
    }
  }

  return _throttle;
};

document.onmousemove = throttle(handleMove, 1000);

3.3 可选是否立即执行v-3

上面的函数第一次默认是立即触发的,如果我们想自己设定第一次是否立即触发该怎么做呢?

优化:

代码语言:javascript复制
const handleMove = (e) => {
  console.log("监听了一次鼠标移动事件", e, this);
};

const throttle = function (fn, interval, leading = true) {
  let nowTime;
  let lastTime = 0;

  function _throttle(...args) {
    nowTime = new Date().getTime();

    // leading为flase表示不希望立即执行函数 
    // lastTime为0表示函数没执行过
    if (!leading && lastTime === 0) {
      lastTime = nowTime;
    }

    if (nowTime - lastTime > interval) {
      fn.apply(this, args);
      lastTime = nowTime;
    }
  }

  return _throttle;
};

document.onmousemove = throttle(handleMove, 3000, false);

3.4 可选最后一次是否执行v-4(最终版本)

如果最后一次监听的移动事件与上一次执行的时间不到设定的时间间隔,函数是不会执行的,但是有时我们希望无论到没到设定的时间间隔都能执行函数,该怎么做呢?

我们的逻辑是:因为我们不知道哪一次会是最后一次,所以每次都设置一个定时器,定时器的时间间隔是距离下一次执行函数的时间;然后在每次进来都清除上次的定时器。这样就能保证如果这一次是最后一次,那么等到下一次执行函数的时候就必定会执行最后一次设定的定时器。

代码语言:javascript复制
const handleMove = (e) => {
  console.log("监听了一次鼠标移动事件", e, this);
};

// trailing用来选择最后一次是否执行
const throttle = function (fn,interval,leading = true,trailing = false) {
  let nowTime;
  let lastTime = 0;
  let timer;

  function _throttle(...args) {
    nowTime = new Date().getTime();
    // leading为flase表示不希望立即执行函数
    // lastTime为0表示函数没执行过
    if (!leading && lastTime === 0) {
      lastTime = nowTime;
    }

    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    if (nowTime - lastTime >= interval) {
      fn.apply(this, args);
      lastTime = nowTime;
      return;
    }

    // 如果选择了最后一次执行 就设置一个定时器
    if (trailing && !timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
        lastTime = 0;
      }, interval - (nowTime - lastTime));
    }
  }

  return _throttle;
};

document.onmousemove = throttle(handleMove, 3000, true, true);

0 人点赞