乐趣区

关于javascript:手写节流防抖函数

1. 意识防抖和节流函数

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

  • 而 javascript 是事件驱动的,大量的操作会触发事件,退出到事件队列中解决
  • 而对于某些频繁的事件处理会造成性能的损耗,咱们就能够通过防抖和节流来限度事件频繁的产生

1.1. 意识防抖 debounce 函数

场景 :在理论开发中,经常会碰到点击一个按钮申请网络接口的场景,这时用户如果因为 手抖 多点了几下按钮,就会呈现短时间内屡次申请接口的状况,实际上这会造成 性能的耗费,咱们其实只须要监听最初一次的按钮,然而咱们并不知道哪一次会是最初一次,就须要做个延时触发的操作,比方这次点击之后的 300 毫秒内没再点击就视为最初一次。这就是防抖函数应用的场景

总结防抖函数的逻辑

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

1.2 意识节流 throttle 函数

场景:开发中咱们会有这样的需要,在鼠标挪动的时候做一些监听的逻辑比方发送网络申请,然而咱们晓得 document.onmousemove 监听鼠标挪动事件触发频率是很高的,咱们心愿依照肯定的频率触发,比方 3 秒申请一次。不论两头 document.onmousemove 监听到多少次只执行一次。这就是节流函数的应用场景

总结节流函数的逻辑

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

2. 实现防抖函数

2.1 根本实现 v -1

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, 还一个是无奈传递传递参数。

优化:

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

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

优化:

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

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

优化:

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 回调

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:咱们设置的距离

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 指向问题 以及 参数传递

优化:

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

下面的函数第一次默认是立刻触发的,如果咱们想本人设定第一次是否立刻触发该怎么做呢?

优化:

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(最终版本)

如果最初一次监听的挪动事件与上一次执行的工夫不到设定的工夫距离,函数是不会执行的,然而有时咱们心愿无论到没到设定的工夫距离都能执行函数,该怎么做呢?

咱们的逻辑是:因为咱们不晓得哪一次会是最初一次,所以 每次都设置一个定时器 ,定时器的工夫距离是间隔下一次执行函数的工夫;而后在每次进来 都革除上次的定时器。这样就能保障如果这一次是最初一次,那么等到下一次执行函数的时候就必定会执行最初一次设定的定时器。

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);
退出移动版