来手写一次防抖节流吧!

前言

在我们日常的项目开发中,常常会进行窗口的 resize、scroll 输入框等操作,当出现高频的操作是容易引发性能问题,那作为性能优化的一把好手,我想大家对 防抖(debounce) 和 节流(throttle) 是熟的不能再熟了吧。

  • 防抖跟节流这俩概念相似常常容易被人混淆
  • 防抖节流在实际项目中也颇为实用,能够起到节省性能的作用
  • 在面试中也是常常出现,手写防抖节流更是难倒了一大片好汉

那就再来看看吧,这一次彻底掌握 防抖 和 节流!

防抖(debounce)

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时,连续触发一个函数,只执行最后一次。

当一个事件频繁触发,而我们希望在事件触发结束一段时间后(此段时间内不再有触发)才实际触发响应函数时会使用防抖(debounce)。例如用户一直点击按钮,但你不希望频繁发送请求,你就可以设置当点击后 200ms 内用户不再点击时才发送请求。
来手写一次防抖节流吧!_第1张图片

应用场景:

  1. 输入用户名、密码;
  2. 搜索框输入;
  3. 浏览器窗口调整大小,只需要在最后一次调整完毕后调用 resize 回调,防止重复渲染。

防抖的实现原理

在防抖函数内部维护一个定时器,通过定时器延迟执行触发函数,每次调用防抖函数都会清除定时器重新开始计时,这样能保证只有最后一次操作能被触发。

防抖的简单实现

先来实现一个最简单的防抖,在定时器的时间结束之后触发 传入的 fn() ,

// 防抖函数
const debounce = (fn, delay) => {
  let timer;
  return function () {
    // 返回的函数中 this 指向触发事件的 window
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function () {
     // 定时器中的 window
     fn()
    }, delay);
  };
};

// 处理函数
function handle() {
  // 处理函数的 this 指向 window
  console.log(Math.random()); 
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));

而此时暂时还能满足当前的需求,此时各个环境中的 this 指向 window 。
如果返回的 fn 函数需要回去事件的参数 e 此时会发生什么情况呢?

// 处理函数
function handle(e) {
  console.log('handle',e);   // undefined
}

在定时器没有为 fn() 传参的情况下, c输出 e 的结果为 undefined,所以需要为 fn 传递参数 args。

那换一种场景,当我们在输入框进行输入操作时,每次的输入会产生一次联想请求,持续高频的输入会造成页面的卡顿,那如何用防抖来进行优化呢?

<div class="haha">
  <input class="debounce" type="text">
div>

<script>

  // 防抖函数
  const debounce = (fn, delay) => {
    let timer;
    return function () {
      const context = this;
      const args = arguments;
      console.log('debounce',this)

      clearTimeout(timer);
      timer = setTimeout(function() {
        console.log('timer',this)

        // 若不改变 this 指向,则会指向 fn 定义环境, window 或者 undefined 
        fn(...args)
      }, delay);
    };
  };

  // 处理函数
  function ajax(e) {
    console.log('handle',this,e.target); 
  }

  // 输入事件
  const debounceInput = document.getElementsByClassName("debounce")[0];
  debounceInput.addEventListener("keyup", debounce(ajax, 500));

script>

此时我们会发现 ajax 中的 this 指向了 window 而不是触发对象。
来手写一次防抖节流吧!_第2张图片

从控制台输出的结果来看,处理函数中的 this 指向了 window ,指向了函数定义时的环境。
在日常的开发中我们常用 call apply 来修改 this 的指向

// 在定时器中改变 fn 中的 this 指向,
// 若不改变 this 指向,会指向 fn 定义环境, window 或者 undefined (严格模式下) 
fn.apply(context, arg);

// 或者

fn.call(context, ...arg)

优化防抖

  1. 解决处理函数的传参问题
    1. 在定时器中为 fn 传入 arguments 参数,让处理函数在执行时能够接收事件参数里的数据。
  2. 解决处理函数中的** this 指向问题**
    1. 若不改变,fn 中的 this 会指向定义时的 this 环境,此时 this 就会指向 window 或 undefined。
    2. 通过 apply 或者 call 来修改 this 指向问题, 确保调用 fn() 的对象与触发事件的对象抱持一致。
// 防抖函数
const debounce = (fn, delay) => {
 // debounceInput 触发防抖时 this 指向 debounceInput 的 dom 对象
  let timer;
  return function () {
    const context = this;
    const args = arguments;
    // 此时返回的函数中 this 指向触发键盘输入的 dom 元素对象 (debounceInput)
    console.log('debounce',this)
    clearTimeout(timer);
    timer = setTimeout(function() {
      console.log('timer',this)
      // 若不改变 this 指向 会指向 fn 定义环境,
      // 修改后的 this 指向触发事件的 dom 对象
      fn.apply(context,args);
    }, delay);
  };
};

image.png在这里插入图片描述

节流(throttle)

当频繁的触发一个事件,每隔一段时间, 只会执行一次事件。

当一个事件频繁触发,而我们希望间隔一定的时间再触发相应的函数时, 就可以使用节流(throttle)来处理。比如判断页面是否滚动到底部,然后展示相应的内容,该功能就可以使用节流实现,在滚动时每300ms进行一次计算判断是否滚动到底部的逻辑,而不用无时无刻地计算。
来手写一次防抖节流吧!_第3张图片

应用场景:

  1. 鼠标不断点击触发,mousedown(单位时间内只触发一次)
  2. 监听滚动事件,触底加载更多,一段时间内请求一次数据展示
  3. 搜索联想,限制用户在输入时每两秒钟响应一次联想
  4. 拖拽时固定时间内只执行一次, 防止高频率的的触发位置变动

节流原理

通过判断是否到达一定时间来触发函数,由于每隔一段时间执行一次,那么就需要计算时间差,我们有两种方式来计算时间差:一种是使用时间戳,另一种是设置定时器。我们有以下三种实现方式:

  • 时间戳
  • 定时器
  • 定时器 + 时间戳

时间戳实现

通过获取当前的时间戳,与上一次的时间戳(初始值为0)进行计算得出两次执行的差值,将时间差与设置的延迟时间进行比较:

  • 当时间差大于等于设置的 delay 时间则执行函数
  • 当时间差小于则继续等待
function throttle(func, delay) {
  let args;
  let lastTime = 0;

  return function () {
    // 获取当前时间
    const currentTime = new Date().getTime();
    args = arguments;
    if (currentTime - lastTime > delay) {
      func.apply(this, args);
      lastTime = currentTime;
    }
  };
}

通过时间戳实现的节流也称之为 **首节流 **,因为首节流在触发事件时立即执行,以后每过 delay 秒之后才执行一次,并且最后一次触发事件若不满足要求不会被执行

定时器实现

在事件触发时设置一个定时器, 当再次触发事件时,

  • 如果定时器存在,直接 return 不进行任何操作,直到当前的定时器执行完毕;
  • 如果定时器不存在则设置定时器,
  • 定时器经过 delay 秒的延时时间后,执行传入的 fn(),最后清空定时器,方便下一次设置定时器。
function throttle1(fn, delay) {
  let timer;
  return function () {
    const context = this;
    let args = arguments;

    if (timer) return;
    timer = setTimeout(function () {
      console.log("hahahhah");
      fn.apply(context, args);

      clearTimeout(timer);
      timer = null;
    }, delay);
  };
}

通过定时器实现的节流被称之为 **尾节流 **,因为尾节流的第一次触发不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数。

定时器 & 时间戳

以上两种方式确实能够实现节流,但是他们都只能实现执行第一次或者最后一次,无法同时满足第一次与最后一次执行的并存。
那么如果将定时器和时间戳相互结合是否能够满足这个需求呢?

  • 根据时间戳判断时间差大于 delay 时间
    • 判断定时器,
// 定时器 + 时间戳  首尾都执行
function throttle(fn, delay) {
  let timer, context, args;
  let lastTime = 0;

  return function () {
    context = this;
    args = arguments;

    let currentTime = new Date().getTime();
    // 清空定时器
    clearTimeout(timer);
    
    // 时间差 大于 delay 时
    if (currentTime - lastTime > delay) {
      // 防止时间戳和定时器重复

      // 清空定时器后直接 执行 fn
      fn.apply(context, args);
      lastTime = currentTime;
    } else {
      timer = setTimeout(() => {
        // 设置定时器 更新执行时间, 防止重复执行
        lastTime = new Date().getTime();
        // 执行后 清空定时器
        fn.apply(context, args);
        
      }, delay);
    }

  };
}

lodash 中的防抖节流

_.debounce 防抖

创建一个防抖函数,从上一次被调用后延迟 wait 毫秒后执行 func 函数。

参数
_.debounce(func, [wait=0], [options=])

  • func: 防抖动的函数
  • wait: 延迟时间
  • option:选项对象
    • leading=false:指定在延迟开始前调用。
    • maxWait:func 允许被延迟的最大时间。
    • trailing=true:指定延迟结束后调用。
  • 该函数提供一个 cancel 方法取消延迟的函数调用以及 flush 的立即调用

使用方式:

// 避免窗口在变动时出现昂贵的计算开销。
jQuery(window).on('resize', _.debounce(calculateLayout, 150));
 
// 当点击时 `sendMail` 随后就被调用。
jQuery(element).on('click', _.debounce(sendMail, 300, {
  'leading': true,
  'trailing': false
}));
 
// 确保 `batchLog` 调用1次之后,1秒内会被触发。
var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
var source = new EventSource('/stream');
jQuery(source).on('message', debounced);
 
// 取消一个 trailing 的防抖动调用
jQuery(window).on('popstate', debounced.cancel);

在vue 项目中的实现

//  在 vue 中的使用实现
refLineDebounce: _.debounce(() => {
  const { vLine, hLine } = self.assistLineParams
  self.vLine = vLine
  self.hLine = hLine
}, 30),
  

_.throttle 节流

创建一个节流函数, 在 wait 秒内最多只执行一次 func() 函数。

参数
_.throttle(func, [wait=0], [options=])

  • func: 要节流的函数。
  • wait: 需要节流的毫秒。
  • options: 选项对象。
    • leading=true: 指定调用在节流开始前。
    • trailing=true: 指定调用在节流结束后。
  • 该函数提供一个 cancel 方法取消延迟的函数调用以及 flush 方法立即调用。
// 避免在滚动时过分的更新定位
jQuery(window).on('scroll', _.throttle(updatePosition, 100));
 
// 点击后就调用 `renewToken`,但5分钟内超过1次。
var throttled = _.throttle(renewToken, 300000, { 'trailing': false });
jQuery(element).on('click', throttled);
 
// 取消一个 trailing 的节流调用。
jQuery(window).on('popstate', throttled.cancel);


在调节窗口大小时可以通过节流来实现

onResize: _.throttle((index, item, x, y, w, h) => {
  item.chartWidth = w
  item.chartHeight = h
}, 100),

总结

防抖和节流都是为了限制了用户的操作次数,避免频繁的触发造成页面的卡顿,从而提高页面的性能。
主要的区别就是:
防抖: 在频繁触发的过程中,只会在最后一次执行(延迟执行)
节流:在固定的时间内只执行一次函数

参考

  1. 老生常谈的函数防抖与节流
  2. 【手写代码】面试官:请你手写防抖和节流
  3. JS的防抖与节流

你可能感兴趣的:(javascript,前端,java)