怎么写一个通用的节流和防抖函数

3,558 阅读5分钟

前言

最近在家里上班有点闲,就刷了些前端面试题,研究一下函数节流和函数防抖。这东西在项目中也常用,但一直是直接从网上copy代码,没深究过,最近就仔细看了下,还是有点收获的,前端面试的时候,常会被问道闭包的知识,节流与防抖就是一个很经典的闭包的应用。

场景

节流的常见场景

在日常开发中,我们可能会遇见这么一种场景,一个元素的宽高需要跟随窗口的变化来变化;例如:当我们使用 element-UI 的表格组件时,表格的数据是不确定的,但我们需要固定表头,当内容过多时可以对内容进行滚动,这时我们就需要给组件提供一个具体的高度,但是浏览器的窗口宽高是会变化的(人为的去缩放浏览器),这就要求我们去监听窗口的变化,当窗口宽高变化的时候,执行一个函数,重新计算浏览器宽高,让表格的高度发生变化

防抖的常见场景

日常开发过程中,我们可能会遇到实时搜索的场景;例如:用户在搜索框输入了内容,我们要根据内容实时的给用户推荐相关的搜索语,让用户选择。

问题

在上述两个场景中,有一个共同点就是函数被高频的调用,无论是实时窗口变化还是输入框的实时变化,都会超高频的触发 resizeinput 事件,但不同的是,在节流的场景中,我们更希望的是在一定的时间段,计算窗口宽高的函数只被执行一次;在防抖的场景中,我们更希望用户输入结束后,再根据输入内容,给用户推荐搜索语。

解决方法

节流的代码解决

对于节流的场景,我们的目标是在一定的时间段内,函数只被执行一次;我们假设在窗口变化过程中,每200毫秒计算一次浏览器宽高,也就是每200毫秒修改一次元素大小


const getWindowHeight = () => {
  const {width,height} = window.screen;
  console.log('浏览器宽度为:',width, '浏览器高度为:', height);
}
// 计算窗口宽高的变化
const throttle = (fn) => {
  let timeNo;
  return () => {
  	console.log('窗口变化');
    if(timeNo) return;
    timeNo = setTimeout(() => {
      fn()
      clearTimeout(timeNo);
      timeNo = null;
    }, 200);
  }
}
// 监听窗口宽高变化
window.addEventListener('resize', throttle(getWindowHeight))

防抖的代码解决

对于防抖的场景,我们的目标是,在高频被触发时,以最后一次触发,去执行函数;我们假定时间间隔是200毫秒,也就是说用户在搜索框输入内容触发 input 事件后,等200毫秒后再去拿输入框内容,在这200毫秒内用户输入了,我们就重新计时等待;

const input = document.querySelector('#input'); // 拿输入框对象
const debounce = () => {
  let timeNo;
  return (e) => {
    clearTimeout(timeNo);
    timeNo = setTimeout(() => {
      const {target: {value}} = e;
      console.log('输入框的值为',value)
      clearTimeout(timeNo);
    },200)
  }
}
input.addEventListener('input', debounce()); // 监听输入框的input事件

概念

函数节流 即在一个事件被连续的触发过程中,以一个固定的时间长度,间隔的去执行一个函数;

函数防抖 即在一个事件被连续的触发过程中,只在最后一次触发时执行一个函数

优化

对函数节流的通用封装

/**
 * 通用版节流函数
 *
 * @param {function} fn - 要被执行的方法, 相隔多长时间要被执行的方法
 * @param {number} Intervals - 间隔时间, 相隔多长时间调用一次对应方法
 * @params {any} args - 剩余参数,剩余参数将会在调用fn时作为参数传给fn
 * @params {any} params - 以下方使用例子看,resize事件被触发的时候,会在传个event对象过去,所以同样需要接收
 * @return function
 */
const throttle = (fn, Intervals, ...args) => {
  let timeNo;
  return (...params) => {
    if(timeNo) return;
    timeNo = setTimeout(() => {
      fn(...args,...params)
      clearTimeout(timeNo);
      timeNo = null;
    }, Intervals);
  }
}
// 使用例子
const obj = {
  name: '张三',
  fun(params, str) {
    console.log(this.name, '接收参数', params, str);
  }
}
window.addEventListener('resize', throttle(obj.fun.bind(this), 200, {text: '最外面传的第一个参数'}, '最外面传的第二个参数'))

对函数防抖的通用封装

/**
 * 通用版防抖函数
 *
 * @param {function} fn - 要被执行的方法, 相隔多长时间要被执行的方法
 * @param {number} Intervals - 间隔时间, 相隔多长时间调用一次对应方法
 * @params {any} args - 剩余参数,剩余参数将会在调用fn时作为参数传给fn
 * @params {any} params - 以下方使用例子看,input事件被触发的时候,会在传个event对象过去,所以同样需要接收
 * @return function
 */
const debounce = (fn, Intervals, ...args) => {
  let timeNo;
  return (...params) => {
    clearTimeout(timeNo);
    timeNo = setTimeout(() => {
      fn(...args,...params)
      clearTimeout(timeNo);
    }, Intervals)
  }
}
// 使用例子
const getInputValue = (e) => {
  const {target: {value}} = e;
  console.log('输入框的值为',value)
}
const input = document.querySelector('#input'); // 拿输入框对象
input.addEventListener('input', debounce(getInputValue,200)); // 监听输入框的input事件

下篇文章: 自定义Vue-Cli项目模板

iframe架构微前端实战 中说了,为了方便之后新开项目,父项目和子项目就可以做成 Vue模板,用Vue-Cli去创建项目时直接去拉取指定的模板,省却再进行认为修改的过程,直接开箱即用,下篇文章计划写如何自定义Vue项目模板。

系列文章

iframe架构微前端实战

恰运维一口饭(Nginx常用配置)

大型前端项目结构设计

大型前端项目git管理方案

如何显著的提升前端加载性能

linux安装nginx及配置(一)

如何实现一个通过js调用使用的消息提示组件(Vue)