前言
虽然防抖和节流已经可以通过全局引入Loadsh实现,但是在面试的时候还是会经常遇见手写防抖和节流.
其实防抖
和节流
不仅仅在面试中会让大家手写,在实际项目中也可以起到性能优化的作用,所以还是很有必要掌握的。一方面它们的应用场景比较多,另一方面它们背后的代码也有一定的难度:高阶函数,定时器,逻辑,下上文等,用来考评面试者的基础功底及实践能力是比较不错的选择。
栗子
<style>
#content {
height: 400px;
width: 400px;
margin: 0 auto;
line-height: 400px;
text-align: center;
color: #fff;
background-color: pink;
font-size: 80px;
}
</style>
<div id="content">
</div>
<script>
let num = 1;
const content = document.getElementById('content');
function count() {
content.innerHTML = num++;
};
content.onmousemove = count
</script>
复制代码
在不做其他处理的情况下,函数被频繁地执行导致页面上数据变化特别快。所以,接下来让我们来看看防抖和节流是如何去解决这个问题的。
这里就不展示效果了,感兴趣的可以复制看一下运行结果
防抖
所谓防抖,就是指触发事件后 n 秒后才执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间
防抖函数分为非立即执行版和立即执行版
非立即执行
<style>
#content {
height: 400px;
width: 400px;
margin: 0 auto;
line-height: 400px;
text-align: center;
color: #fff;
background-color: pink;
font-size: 80px;
}
</style>
<div id="content">
</div>
<script>
/**
* @description: 防抖函数 非立即执行
* @param {type} fn 函数
* @param {type} wait 延迟时间 单位毫秒
* @return: 返回 延时函数
*/
// 非立即执行
let num = 1;
const content = document.getElementById('content');
function debounce(fn, wait) {
let timerID
return function () {
// 如果有定时器id清除定时器
if (timerID) clearTimeout(timerID)
timerID = setTimeout(() => {
// 修改this指向
fn()
}, wait)
}
}
function count() {
content.innerHTML = num++;
};
content.onmousemove = debounce(count, 1000)
</script>
复制代码
-
防抖功能就完成了,但是这样会有一个问题,如果我们在
count()
打印this
会发现我们这样执行的this
是指向Window
的 -
不是我们所希望的,我们需要使用
apply
来改变this
指向,再者就是我们需要考虑到执行函数的参数,因为不同的函数肯定会有不同的参数传入,对于参数我们可以使用arguments
处理
完整版
<style>
#content {
height: 400px;
width: 400px;
margin: 0 auto;
line-height: 400px;
text-align: center;
color: #fff;
background-color: pink;
font-size: 80px;
}
</style>
<div id="content">
</div>
<script>
/**
* @description: 防抖函数 非立即执行
* @param {type} fn 函数
* @param {type} wait 延迟时间 单位毫秒
* @return: 返回 延时函数
*/
// 非立即执行
let num = 1;
const content = document.getElementById('content');
function debounce(fn, wait) {
let timerID
return function () {
// this指向依然指向原来的函数
const context = this
// // 不同的函数会有不同的参数传入,对于参数我使用arguments处理。
const args = [...arguments]
// 如果有定时器id清除定时器
if (timerID) clearTimeout(timerID)
timerID = setTimeout(() => {
// 修改this指向
fn.apply(context, args)
}, wait)
}
}
function count() {
content.innerHTML = num++;
};
content.onmousemove = debounce(count, 1000)
</script>
复制代码
非立即执行意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
立即执行
立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。
<style>
#content {
height: 400px;
width: 400px;
margin: 0 auto;
line-height: 400px;
text-align: center;
color: #fff;
background-color: pink;
font-size: 80px;
}
</style>
<div id="content">
</div>
<script>
/**
* @description: 防抖函数 立即执行
* @param {type} fn 函数
* @param {type} wait 延迟时间 单位毫秒
* @return: 返回 延时函数
*/
// 立即执行
let num = 1;
const content = document.getElementById('content');
function debounce(fn, wait) {
let timerID
return function () {
// this指向依然指向原来的函数
const context = this
// 不同的函数会有不同的参数传入,对于参数我使用arguments处理。
const args = [...arguments]
// 如果有定时器id清除定时器
if (timerID) clearTimeout(timerID)
const callNow = !timerID
timerID = setTimeout(() => {
// 相当于清空定时器
timerID = null
}, wait)
// 没有定时器id 修改this指向
if (callNow) fn.apply(context, args)
}
}
function count() {
content.innerHTML = num++;
};
content.onmousemove = debounce(count, 1000)
</script>
复制代码
合并
在开发过程中,我们需要根据不同的场景来决定我们需要使用哪一个版本的防抖函数,一般来讲上述的防抖函数都能满足大部分的场景需求。但我们也可以将非立即执行版和立即执行版的防抖函数结合起来
<style>
#content {
height: 400px;
width: 400px;
margin: 0 auto;
line-height: 400px;
text-align: center;
color: #fff;
background-color: pink;
font-size: 80px;
}
</style>
<div id="content">
</div>
<script>
/**
* @description: 防抖函数 非立即执行
* @param {type} fn 函数
* @param {type} wait 延迟时间 单位毫秒
* @param {type} promptly true : 立即 false 非立即
* @return: 返回 延时函数
*/
// 非立即执行
let num = 1;
const content = document.getElementById('content');
function debounce(fn, wait, promptly) {
let timerID
return function () {
// this指向依然指向原来的函数
const context = this
// // 不同的函数会有不同的参数传入,对于参数我使用arguments处理。
const args = [...arguments]
// 如果有定时器id清除定时器
if (timerID) clearTimeout(timerID)
if (promptly) {
const callNow = !timerID
timerID = setTimeout(() => {
// 相当于清空定时器
timerID = null
}, wait)
// 没有定时器id 修改this指向
if (callNow) fn.apply(context, args)
} else {
timerID = setTimeout(() => {
// 修改this指向
fn.apply(context, args)
}, wait)
}
}
}
function count() {
content.innerHTML = num++;
console.log(this);
};
content.onmousemove = debounce(count, 1000, true)
</script>
复制代码
节流
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。 节流会稀释函数的执行频率。
对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。
时间戳
<style>
#content {
height: 400px;
width: 400px;
margin: 0 auto;
line-height: 400px;
text-align: center;
color: #fff;
background-color: pink;
font-size: 80px;
}
</style>
<div id="content">
</div>
<script>
/**
* @description: 节流函数 时间戳
* @param {type} fn 函数
* @param {type} wait 延迟时间 单位毫秒
* @return: 返回 延时函数
*/
// 时间戳
let num = 1;
const content = document.getElementById('content');
function throttle(fn, wait, promptly) {
let previous = 0
return function () {
let now = Date.now()
// this指向依然指向原来的函数
let context = this
// // 不同的函数会有不同的参数传入,对于参数我使用arguments处理。
let args = [...arguments]
if (now - previous > wait) {
// 修改this指向
fn.apply(context, args)
// 修改时间为当前时间
previous = now
}
}
}
function count() {
content.innerHTML = num++;
};
content.onmousemove = throttle(count, 1000)
</script>
复制代码
在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次。
定时器版
<style>
#content {
height: 400px;
width: 400px;
margin: 0 auto;
line-height: 400px;
text-align: center;
color: #fff;
background-color: pink;
font-size: 80px;
}
</style>
<div id="content">
</div>
<script>
/**
* @description: 节流函数 定时器版
* @param {type} fn 函数
* @param {type} wait 延迟时间 单位毫秒
* @return: 返回 延时函数
*/
// 定时器版
let num = 1;
const content = document.getElementById('content');
function throttle(fn, wait) {
let timerID;
return function () {
// this指向依然指向原来的函数
let context = this;
// 不同的函数会有不同的参数传入,对于参数我使用arguments处理
let args = [...arguments];
if (!timerID) {
timerID = setTimeout(() => {
timerID = null;
fn.apply(context, args)
}, wait)
}
}
}
function count() {
content.innerHTML = num++;
};
content.onmousemove = throttle(count, 1000)
</script>
复制代码
在持续触发事件的过程中,函数不会立即执行,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次
其实时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候
和防抖函数一样,也可以进行合并
合并
<style>
#content {
height: 400px;
width: 400px;
margin: 0 auto;
line-height: 400px;
text-align: center;
color: #fff;
background-color: pink;
font-size: 80px;
}
</style>
<div id="content">
</div>
<script>
//**
* @description: 节流函数 合并版
* @param {type} fn 函数
* @param {type} wait 延迟时间 单位毫秒
* @param {type} type 1 表时间戳版,2 表定时器版
* @return: 返回 延时函数
*/
let num = 1;
const content = document.getElementById('content');
function throttle(fn, wait, type) {
if (type === 1) {
let previous = 0;
} else if (type === 2) {
let timerID;
}
return function () {
// this指向依然指向原来的函数
let context = this;
// 不同的函数会有不同的参数传入,对于参数我使用arguments处理
if (type === 1) {
let now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
// 修改时间为当前时间
previous = now;
}
} else if (type === 2) {
if (!timerID) {
timerID = setTimeout(() => {
timerID = null;
func.apply(context, args)
}, wait)
}
}
}
}
function count() {
content.innerHTML = num++;
};
content.onmousemove = throttle(count, 1000)
</script>
复制代码
总结
- 对于
防抖
和节流
一个最主观的判断方法就是:防抖则会只执行一次,而节流则会每隔一段时间执行一次
-
其实本质上都是为了节省程序的性能(防止高频函数调用)
-
借助了闭包的特性来缓存变量(状态)
-
都可以使用setTimeout实现