帮你彻底搞懂防抖和节流(附带在React使用的一个例子)

1. 前言

开门见山,使用防抖节流技术的意义:节约资源,提升用户体验点这里可视化感受

浏览器中有许多事件会在很小时间间隔内频繁触发,比如:监听用户的输入(keyup、keydown)、浏览器窗口调整大小和滚动行为(resize、scroll)、鼠标的移动行为(mousemove)等。如果这些事件一触发我们就执行相应的事件处理函数的话,那将会造成较大的资源浪费或者给用户带来不好的体验。
例如,我们为输入框绑定keyup回调函数,判断用户输入的手机号是否符合规则,在普通情况下,用户每输入一个字符,我们就会对当前的输入内容进行判断并给出相应的提示,但实际上用户的输入并未结束,我们就在页面中给出输入不符合规则的错误提示,这样的用户体验很不好,并且这么频繁的验证没必要。实际中我们可能采用的处理方式是:当用户在停止输入一段时间后我们再判断输入的内容是否符合规则。(后面我们使用React来实现该例子)

这时,我们的防抖节流两位小兄弟就排上用场了!

2. 防抖(debounce)

防抖:触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。
思路:每次触发事件时都取消之前的延时调用方法。
使用的本质:不允许某一行为触发。
一种简单的实现方式如下(更完善的可以参考:lodash的debounce):

function debounce(fn, ms) {
	let timerId // 创建一个标记用来存放定时器的返回值
	return function () {
		timerId && clearTimeout(timerId) // 每当用户输入的时候把前一个 setTimeout clear 掉
		// 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
		timerId = setTimeout(() => { 
			fn.apply(this, arguments)
		}, ms)
	}
}


// 使用例子
function sayHi() {
	console.log('防抖成功');
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi, 1000)); // 防抖

防抖一般用于input输入框。

3. 节流(throttle)

节流: 高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率。
思路:每次触发事件时都判断当前是否有等待执行的延时函数。
使用的本质:允许某一行为触发,但是触发的频率不能太高。
一种简单的实现方式如下(更详细的可以参考:lodash的throttle):

function throttle(fn, ms) {
	let timerId // 创建一个标记用来存放定时器的id
	return function () {
		// 没有定时器等待执行,则表示可以创建新的定时器来执行函数
		if (!timerId) {
			timerId = setTimeout(() => {
				// 定时器id清空,表示可以执行下一次调用了
				timerId = null
				fn.apply(this, arguments)
			}, ms)
		}
	}
}


// 使用例子
function sayHi(e) {
	console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi, 1000));

节流一般用于动画相关的场景。

4. 两个比喻来帮助区分防抖和节流

我们从词本身的意义展开来解释防抖和节流,希望能帮助大家更清楚地区分它们。

防抖:防止抖动的意思,也就是不抖动时才进行相应的处理。比如一根拉直的弹簧,我们拨动一下它就会抖动,过一段时间后弹簧会恢复到平静的状态(从拨动弹簧使其抖动到恢复平静的时长就是代码例子的ms值)。在这个过程中,拨动弹簧的这一行为假设为事件被触发(代码中的input事件被触发),当弹簧恢复平静时我们再执行事件处理函数(代码中的sayHi函数)。基于以上假设,当我们在弹簧还没恢复到平静状态时,又不断地拨动它(清除了原来的setTimeout,并重新开始计时),因为弹簧还没恢复到平静,那么事件处理函数就一直不会被执行。只有当我们拨动它,并且之后再也不动它(也就是最后一次触发),等它恢复到平静状态时(setTimeout时间到达),事件处理函数才会被执行。

节流:控制住流量的意思,流量没达到一定的程度就不进行相应的处理。比如我们用水桶去接水,水龙头保持以不变的流量出水(即事件不断被触发),只有当水桶里的水满的时候(setTimeout时间到达),我们才将装满水的水桶拿走(执行事件处理函数),使用完后再拿这个空桶继续接水(重新开始计时)。

从以上的比喻中我们可以知道,防抖是用来处理那些离散的事件(拨动弹簧),节流是用来处理那些连续的事件(水一直在流出),这样我们就可以根据事件触发是离散型的还是连续型的来判断使用防抖还是节流啦(当然还要考虑实际需求)!
防抖应用的例子:判断用户的输入情况,只在用户停止输入一段时间后再进行判断。
节流应用的例子:滚动页面垂直滚动条,判断是否滚动到页面底部。

5. 在React中使用

5.1 未使用防抖

import * as React from 'react'
import './App.css'

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      tip: null,
      trigerTimes: 1
    }
  }
  handleKeyUp = (e) => {
    this.isPhoneLegal(e.target.value) // 对用户输入进行判断
  }

  isPhoneLegal = (phone) => {
    const phoneRegexp = /^1([38]\d|5[0-35-9]|7[3678])\d{8}$/  //手机号码的正则表达式
    const { trigerTimes } = this.state
    if(phoneRegexp.test(phone)) {
      this.setState({
        tip: `手机号符合规则!`,
        trigerTimes: 0
      })
    } else {
      this.setState({
        tip: `手机号有误, 触发了:${trigerTimes}`,
        trigerTimes: trigerTimes + 1
      })
    }
  }

  render() {
    return (
      <div className="container">
        <input onKeyUp={ this.handleKeyUp } placeholder="请输入手机号"/>
        <span>
          {this.state.tip}
        </span>
      </div>
    )
  }
}

export default App

运行上述代码,得到的结果如下:
未使用防抖
可以看到,我们每输入一个字符,keyup事件就被触发一次,用户未输入完成就提示输入有误,这种体验不是很好。

5.2 使用防抖

import * as React from 'react'
import './App.css'

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      tip: null,
      trigerTimes: 1
    }
    this.isPhoneLegal = debounce(this.isPhoneLegal, 1000)
  }
  handleKeyUp = (e) => {
    this.isPhoneLegal(e.target.value) // 对用户输入进行判断
  }
  isPhoneLegal = (phone) => {
    const phoneRegexp = /^1([38]\d|5[0-35-9]|7[3678])\d{8}$/
    const { trigerTimes } = this.state
    if(phoneRegexp.test(phone)) {
      this.setState({
        tip: `手机号符合规则!`,
        trigerTimes: 0
      })
    } else {
      this.setState({
        tip: `手机号有误, 触发了:${trigerTimes}`,
        trigerTimes: trigerTimes + 1
      })
    }
  }

  render() {
    return (
      <div className="container">
        <input onKeyUp={ this.handleKeyUp} placeholder="请输入手机号"/>
        <span>
          {this.state.tip}
        </span>
      </div>
    )
  }
}

function debounce(fn, ms) {
  let timeoutId
  return function () {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => {
      fn.apply(this, arguments)
    }, ms)
  }
}

export default App

运行上面代码,得到如下结果:
使用防抖
结果显示,此时并不会在用户每次输入字符的时候都进行规则判断。我们在输入到第10位时停顿了一下,然后才执行判断,输出手机号不符合规则的信息。很明显,使用防抖以后,回调执行的次数大大减少了,这样有利于节约资源,提升用户体验。假设这是一个判断用户名是否存在的输入框,那么我们的事件触发回调就需要进行Ajax请求,后端查询数据库判断用户名是否存在,此时,减少回调函数的调用次数可以大大减少网络请求数,降低服务器的压力。

补充:
这是一篇很不错的文章值得学习:Debouncing and Throttling Explained Through Examples

怎么样,学会了防抖和节流了吗?

若对你有帮助,可以支持一下作者创作更多好文章哦~
赞赏码

文中部分内容参考自这些文章:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码飞_CC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值