JavaScript是事件驱动的,大量的操作触发事件,加入到事件队列中处理。
有一些比较频繁的事件处理就会造成性能损耗,我们就可以通过防抖和节流来限制事件频繁发生,所以防抖和节流也是性能优化的方式之一。
目录
前面说明了防抖和节流是用来限制事件频繁发生的,还不清除防抖和节流的概念,下面先来理解概念。
我们先用一幅图来理解一下防抖的过程。

蓝色的柱子代表事件触发,而橙色的柱子代表响应函数触发。
上图可以看到无论事件触发得多频繁,最后响应函数都是要等待一段固定的时间后才会触发。
事件触发就好像在哭闹的小孩子闹的次数,只有小孩安静下来一段时间之后才可以给小孩糖吃。如果一直哭闹,就一直推后吃糖的时间。
总结:
当事件被触发 n 秒后再执行回调,如果在 n 秒内又被触发,则重新计时
防抖函数的应用场景有很多,有时候防抖和节流两种方案在同样的场景都可以实现性能优化,主要看具体的需求。
适合防抖函数的场景:
输入框使用防抖函数可能是开发时应用比较多的场景,下面举输入框场景的案例让对防抖函数的理解更深。
我们都可能会遇到这样的场景,在一些购物网站的搜索框输入想要的商品。

在搜索框下面的联想商品是随着网站的加载一起下载下来的吗?
在上面输入JavaScript高级,单单JavaScript就10个单词了,难道需要发送10次网络请求吗?
通过上面的两个问答可以得出,我们应该要在用户输入缓慢或者停下来一段时间再发送网络请求;
比如用户在快速输入JavaScript的时候可以只发送一次网络请求,给用户输入内容的时间,用户在输入的时候没有结果也不会觉得奇怪。
我们可以先实现简单的防抖函数,后面有其他的需求可以加上去。只要会实现基本的防抖函数就足够应对面试题了。
- <input type="text">
- <script>
- function mydebounce(fn, delay) {
- //1.用于记录上一次时间触发的timer
- let timer = null
- //2.触发事件时执行的函数
- const _debounce = () => {
- //2.1.如果有再次触发事件,需要取消上一次的事件
- if(timer) clearTimeout(timer)
- //2.2.延迟去执行对应的fn函数(传入的回调函数)
- timer = setTimeout(()=> {
- fn()
- timer = null // 执行函数之后,将timer重新置为null
- }, delay)
- }
-
- return _debounce
- }
- script>
- <script>
- const inputEl = document.querySelector("input")
- let count = 0
- const inputChange = function() {
- count++
- console.log("发送网络请求",count)
- }
- //实现防抖
- inputEl.oninput = mydebounce(inputChange, 1000)
- script>
我们模拟搜索框发送网络请求的场景。




至此,最基本的防抖函数就实现了,在输入框输入了内容,过了一段时间才发送了一次网络请求。

最基本的防抖函数便于理解防抖函数的原理,对于初学者来说,上面的理解完已经足够了。但是还有一些细节没有实现。这些优化应用场景比较少,或者说更高阶,可以只了解。
- function mydebounce(fn, delay) {
- let timer = null
- const _debounce = () => {
- if(timer) clearTimeout(timer)
- timer = setTimeout(()=> {
- fn()
- timer = null
- }, delay)
- }
-
- return _debounce
- }
-
- //实现防抖
- inputEl.oninput = mydebounce(function() {
- count++
- console.log("发送网络请求", count, this.value)
- }, 1000)
上面是自己实现防抖的函数,模拟发送网络请求的函数作为mydebounce的第一个参数(fn),fn在mydebounce函数中是独立调用的。
所以this是指向window的,this.value为undefined,不想this指向window,我们需要自己绑定this。

如果不清楚this的绑定规则,可以先看一下JavaScript中this指向。
_debounce是一个箭头函数,是没有this的,我们将_debounce改为普通函数,并且在fn的调用时通过apply显式绑定this。
- function mydebounce(fn, delay) {
- let timer = null
- const _debounce = function(){
- if(timer) clearTimeout(timer)
- timer = setTimeout(()=> {
- fn.apply(this)
- }, delay)
- }
-
- return _debounce
- }

这样this.value就有值了。
有时候我们需要拿到event,但是自己实现的防抖函数没有event这个参数。
其实oninput是将event传递到_debounce的参数了的,所以我们将其他参数(args)一起绑定到fn中。
- function mydebounce(fn, delay) {
- let timer = null
- const _debounce = function(...args) {
- if(timer) clearTimeout(timer)
- timer = setTimeout(()=> {
- fn.apply(this, [args])
- timer = null
- }, delay)
- }
-
- return _debounce
- }
-
- //实现防抖
- inputEl.oninput = mydebounce(function(event) {
- count++
- console.log("发送网络请求", count, this.value, event)
- }, 1000)

event参数就有值了。
可以实现防抖函数,也需要实现防抖函数的取消,我们只需要拿到timer,取消掉timer就可以实现防抖函数的取消了。
- function mydebounce(fn, delay) {
- let timer = null
- const _debounce = function(...args) {
- if(timer) clearTimeout(timer)
- timer = setTimeout(()=> {
- fn.apply(this, [args])
- timer = null
- }, delay)
- }
- //3.给_debounce绑定一个取消的函数
- _debounce.cancel = function() {
- if(timer) clearTimeout(timer)
- timer = null
- }
-
- return _debounce
- }
-
- //实现防抖
- const debounceFn = mydebounce(function(event) {
- count++
- console.log("发送网络请求", count, this.value, event)
- }, 1000)
- inputEl.oninput = debounceFn
给_debounce绑定一个取消的函数,如果有定时器就清除定时器。 
在使用这个cancel函数的时候,可以用点击事件来触发取消。
- cancelBtn.onclick = function() {
- debounceFn.cancel()
- }
有时候可能会有这种需求,想要用户在输入第一个内容的时候,就直接执行一次函数(比如发送一次网络请求),后面的内容再防抖。
- function mydebounce(fn, delay, immediate = false) {
- let timer = null
- //isInvoke用来记录是否执行过
- let isInvoke = false
- const _debounce = function(...args){
- if(timer) clearTimeout(timer)
-
- //第一次操作不需要延迟
- if(immediate && !isInvoke) {
- fn.apply(this, [args])
- isInvoke = true
- return
- }
-
- timer = setTimeout(()=> {
- fn.apply(this, [args])
- timer = null
- isInvoke = false //执行函数之后,将isInvoke重新置为false
- }, delay)
- }
- //3.给_debounce绑定一个取消的函数
- _debounce.cancel = function() {
- if(timer) clearTimeout(timer)
- timer = null
- isInvoke = false
- }
-
- return _debounce
- }
我们可以传入一个immediate参数用来判断是否要立即执行第一次函数,如果true,就立即执行第一次函数;false就不执行第一次函数。同时定义一个isInvoke变量来记录是否已经执行过第一次函数了。
在用户设置了immediate为true并且没有执行过第一次函数(即isInvoke为false)的时候,不设置定时器。
其他的函数执行完之后,isInvoke都要重新设置为false。
当设置immediate为true的时候,就实现了这个功能。
- inputEl.oninput = mydebounce(function(event) {
- count++
- console.log("发送网络请求", count, this.value, event)
- }, 1000, true)

相同的,我们依然先用一幅图来理解一下节流的过程。

蓝色的柱子代表事件触发,而橙色的柱子代表响应函数触发。
上图可以看到第一次事件触发会触发响应函数,后面的事件触发无论触发多频繁,都需要等等待时间过后才会再次触发一次响应函数。一段时间内只会触发一次响应函数。
事件触发就好像小孩子不断请求吃糖,第一次满足他吃糖的需求,后面再怎么请求,都不给他吃,要等第二天才能吃第二次糖。
总结:
在一个单位时间内,触发事件至多只能触发一次响应函数
不知道有没有喜欢玩游戏的同学有这样的感受,节流好像是普通攻击的内置cd(冷却)一样,不论你点普通攻击再快,在内置cd时间到之前,你也普攻不出去。
还有一些其他的应用场景,比如:
我们可能玩过这样的游戏:飞机大战。

点击空格就发射出子弹,是不是按得越快发射的子弹越多?
这就是节流的操作:触发了多次事件,在单位时间内,响应函数只会触发一次。
节流函数会比防抖函数难理解一点,我们使用按钮的点击来模拟飞机大战中频繁点击发射子弹的场景。
和防抖函数一样,优先把节流函数的基本实现学习清楚, 再考虑节流函数的功能拓展。
- <button>点击button>
- <script>
- function mythrottle(fn, interval) {
- //1.设置上次触发的时间
- let lastTime = 0
- //2.触发事件时执行的函数
- const _throttle = function() {
- //2.1.获取当前时间
- const nowTime = new Date().getTime()
- console.log(nowTime)
- //2.2.获取等待的时间 时间间隔-(当前时间 - 上次触发的时间)
- const waitTime = interval - (nowTime - lastTime)
-
- if(waitTime <= 0){
- fn()
- //2.3.把当前时间赋值给上次响应的时间
- lastTime = nowTime
- }
- }
- return _throttle
- }
- script>
- <script>
- const buttonEl = document.querySelector("button")
- let count = 0
-
- //实现节流
- const throttleFn = function(){
- count++
- console.log("响应次数", count)
- }
-
- buttonEl.onclick = mythrottle(throttleFn, 1000)
-
- script>
确实是比较难理解,请多点耐心。我们用点击按钮来模拟飞机大战时发射子弹高频触发的场景。点击按钮是事件触发,比如点击许多次发射子弹的按钮,可以触发很多很多次但不响应;事件响应是实际响应,比如发射了子弹就是事件响应了。




上面的公式waitTime = interval - (nowTime - lastTime)太难理解了,我们分成三种情况讨论。
第一种情况是第一次触发:
而(interval)一般都不可能设置超过(nowTime)的值。
(假设interval为1000ms,nowTime为1661345294068,还需等待的时间waitTIme为负数,上面的单位皆为ms)第二种情况是未超过时间间隔触发:

根据公式waitTime = interval - (nowTime - lastTime),需要等待的时间超过0,所以不会执行fn()。
(假设interval为1000ms,还需等待的时间waitTime为正数,上面的单位皆为ms)第三种情况是超过时间间隔触发:
根据公式waitTime = interval - (nowTime - lastTime),两次事件触发时间间隔大于等于interval就是nowTime - lastTime大于interval,所以waitTime小于等于0,可以响应事件,执行fn()。
(假设interval为1000ms,还需等待的时间waitTime为负数,上面的单位皆为ms)上面的节流函数的解释我觉得已经很详细了,先把上面的理解了再学习怎么优化。
- function mythrottle(fn, interval) {
- let lastTime = 0
- const _throttle = function() {
- const nowTime = new Date().getTime()
- const waitTime = interval - (nowTime - lastTime)
-
- if(waitTime <= 0){
- fn()
- lastTime = nowTime
- }
- }
- return _throttle
- }
这是上面自己实现节流函数的代码,_throttle函数已经是普通函数了,有自己的this。所以在执行fn()函数的时候用apply显式绑定this。![]()
- function mythrottle(fn, interval) {
- let lastTime = 0
- const _throttle = function() {
- const nowTime = new Date().getTime()
- const waitTime = interval - (nowTime - lastTime)
-
- if(waitTime <= 0){
- fn.apply(this)
- lastTime = nowTime
- }
- }
- return _throttle
- }
显式绑定了this,this就有值了。
- //实现节流
- const throttleFn = function(){
- count++
- console.log("响应次数", count, this)
- }
-
- buttonEl.onclick = mythrottle(throttleFn, 1000)

有时候我们需要拿到event,但是自己实现的节流函数没有event这个参数。
其实onclick是将event传递到_throttle的参数了的,所以我们将其他参数(args)一起绑定到fn中。
- function mythrottle(fn, interval) {
- //1.上次触发的时间
- let lastTime = 0
- //2.触发事件时执行的函数
- const _throttle = function(...args) {
- //2.1.获取当前时间
- const nowTime = new Date().getTime()
- console.log(nowTime)
- //2.2.获取等待的时间 时间间隔-(当前时间 - 开始时间)
- const waitTime = interval - (nowTime - lastTime)
-
- if(waitTime <= 0){
- fn.apply(this, [args])
- //2.3.把当前时间赋值给开始时间
- lastTime = nowTime
- }
- }
- return _throttle
- }
- //实现节流
- const throttleFn = function(event){
- count++
- console.log("响应次数", count, this, event)
- }
-
- buttonEl.onclick = mythrottle(throttleFn, 1000)

这样就可以拿到event了,当然还有其他的参数。
我们可以使用第三方库来实现防抖和节流操作;underscore还在维护而且功能也比较完善。
Underscore的官网:https://underscorejs.org/
Underscore有几种安装方式:
1.下载Underscore,本地引入;

右键在新的标签页打开第一个链接。复制里面的内容。

创建一个js文件,把代码粘贴进去。

<script src="./防抖和节流.js">script>
2.通过CDN直接引入;

<script src="https://cdn.jsdelivr.net/npm/underscore@latest/underscore-umd-min.js">script>
3.通过包管理工具(npm)管理安装 ;

npm install underscore
我们编写一个案例来模拟一下搜索框的场景怎么使用防抖和节流函数。
防抖函数的使用
- <input type="text">
- <script src="https://cdn.jsdelivr.net/npm/underscore@latest/underscore-umd-min.js">
- script>
- <script>
- //获取input元素
- const inputEl = document.querySelector("input")
- //记录网络请求的次数
- let count = 0
- const inputChange = function() {
- count++
- //模拟网络请求
- console.log("发送网络请求",count)
- }
- //实现防抖
- inputEl.oninput = _.debounce(inputChange, 1000)
-
- script>
_.debounce(要实现防抖的函数,延迟间隔ms)

在输入完成之后,等待了1秒才发送了网络请求,在实际开发中时间可以短一点,比如300ms就差不多了。
节流函数的使用
- <input type="text">
- <script src="https://cdn.jsdelivr.net/npm/underscore@latest/underscore-umd-min.js">
- script>
- <script>
- //获取input元素
- const inputEl = document.querySelector("input")
- //记录网络请求的次数
- let count = 0
- const inputChange = function() {
- count++
- //模拟网络请求
- console.log("发送网络请求",count)
- }
- //实现节流
- inputEl.oninput = _.throttle(inputChange, 1000)
-
- script>
_.throttle(要实现节流的函数,间隔ms)

输入第一个a后,发送了第一次网络请求,后面的1秒内不论输入了几个a,还是没有发送网络请求,等到1秒时间到的时候才发送了第二次网络请求。