【手写代码】面试官:请你手写防抖和节流

9,783 阅读5分钟

一、前言

当用户高频触发某一事件时,如窗口的resize、scroll,输入框内容校验等,此时这些事件调用函数的频率如果没有限制,可能会导致响应跟不上触发,出现页面卡顿,假死现象。此时,我们可以采用 防抖(debounce)节流(throttle) 的方式来减少调用频率,同时又不影响实际效果。

二、防抖

假设你用手压住一个弹簧,那么弹簧不会弹起来,除非你松手。

函数防抖,就是指触发事件后,函数在 n 秒后只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数的执行时间。

简单的说,当一个函数连续触发,只执行最后一次。

函数防抖一般用在什么情况之下呢?一般用在,连续的事件只需触发一次回调的场合。具体有:

  1. 搜索框搜索输入。只需用户最后一次输入完,再发送请求;
  2. 用户名、手机号、邮箱输入验证;
  3. 浏览器窗口大小改变后,只需窗口调整完后,再执行resize事件中的代码,防止重复渲染。

代码实现

在下面这段代码中,我们实现了最简单的一个防抖函数,我们设置一个定时器,你重复调用一次函数,我们就清除定时器,重新定时,直到在设定的时间段内没有重复调用函数。

// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
  // timer是一个定时器
  let timer = null;
  // 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
  return function () {
    // 调用一次就清除上一次的定时器
    clearTimeout(timer);
    // 开启这一次的定时器
    timer = setTimeout(() => {
      fn();
    }, delay)
  }
}

代码优化

仔细一想,上面的代码是不是有什么问题?

问题一: 我们返回的fn函数,如果需要事件参数e怎么办?事件参数被debounce函数保存着,如果不把事件参数给闭包函数,若fn函数需要e我们没给,代码毫无疑问会报错。

问题二: 我们怎么确保调用fn函数的对象是我们想要的对象?你发现了吗,在上面这段代码中fn()函数的调用者是fn所定义的环境,这里涉及this指向问题,想要了解为什么可以去了解下js中的this。

为了解决上述两个问题,我们对代码优化如下

// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
  // timer是一个定时器
  let timer = null;
  // 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
  return function () {
    // 保存事件参数,防止fn函数需要事件参数里的数据
    let arg = arguments;
    // 调用一次就清除上一次的定时器
    clearTimeout(timer);
    // 开启这一次的定时器
    timer = setTimeout(() => {
      // 若不改变this指向,则会指向fn定义环境
      fn.apply(this, arg);
    }, delay)
  }
}

三、节流

当水龙头的水一直往下流,这十分的浪费水,所以我们可以把龙头关小一点,让水一滴一滴往下流,每隔一段时间掉下来一滴水。

节流就是限制一个函数在一段时间内只能执行一次,过了这段时间,在下一段时间又可以执行一次。应用场景如:

  1. 输入框的联想,可以限定用户在输入时,只在每两秒钟响应一次联想。
  2. 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
  3. 表单验证
  4. 按钮提交事件。

代码实现1(时间戳版:首节流)

// 方法一:时间戳
function throttle(fn, delay = 1000) {
  // 记录第一次的调用时间
  var prev = null;
  console.log(prev);
  // 返回闭包函数
  return function () {
    // 保存事件参数
    var args = arguments;
    // 记录现在调用的时间
    var now = Date.now();
    // console.log(now);
    // 如果间隔时间大于等于设置的节流时间
    if (now - prev >= delay) {
      // 执行函数
      fn.apply(this, args);
      // 将现在的时间设置为上一次执行时间
      prev = now;
    }
  }
}

触发事件时立即执行,以后每过delay秒之后才执行一次,并且最后一次触发事件若不满足要求不会被执行

代码实现2(定时器版:尾节流)

// 方法二:定时器
function throttle(fn, delay) {
  // 重置定时器
  let timer = null;
  // 返回闭包函数
  return function () {
    // 记录事件参数
    let args = arguments;
    // 如果定时器为空
    if (!timer) {
      // 开启定时器
      timer = setTimeout(() => {
        // 执行函数
        fn.apply(this, args);
        // 函数执行完毕后重置定时器
        timer = null;
      }, delay);
    }
  }
}

第一次触发时不会执行,而是在delay秒之后才执行,当最后一次停止触发后,还会再执行一次函数。

代码实现3(时间戳 & 定时器)

// 方法三:时间戳 & 定时器
function throttle(fn, delay) {
  // 初始化定时器
  let timer = null;
  // 上一次调用时间
  let prev = null;
  // 返回闭包函数
  return function () {
    // 现在触发事件时间
    let now = Date.now();
    // 触发间隔是否大于delay
    let remaining = delay - (now - prev);
    // 保存事件参数
    const args = arguments;
    // 清除定时器
    clearTimeout(timer);
    // 如果间隔时间满足delay
    if (remaining <= 0) {
      // 调用fn,并且将现在的时间设置为上一次执行时间
      fn.apply(this, args);
      prev = Date.now();
    } else {
      // 否则,过了剩余时间执行最后一次fn
      timer = setTimeout(() => {
        fn.apply(this, args)
      }, delay);
    }
  }
}

第一次会马上执行,最后一次也会执行

本文章已收录JavaScript技术详解,更多JavaScript技术可查看专栏。 ❤码字不易点个免费的赞吧❤