「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战」
这两个东西都以闭包的形式存在。
它们通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 setTimeout 来控制事件的触发频率。
一. 节流 Throttle: 第一个人说了算
节流,即节省交互沟通。流,可理解为交流,不一定会产生网络流量。
例如,drag 的回调,上传进度的回调,都可以设置一个固定的频率,没必要那么频繁。
throttle 的中心思想在于:在某段时间内,不管你触发了多少次回调,我都只认第一次,并在计时结束时给予响应。
先给大家讲个小故事:现在有一个旅客刚下了飞机,需要用车,于是打电话叫了该机场唯一的一辆机场大巴来接。司机开到机场,心想来都来了,多接几个人一起走吧,这样这趟才跑得值——我等个十分钟看看。于是司机一边打开了计时器,一边招呼后面的客人陆陆续续上车。在这十分钟内,后面下飞机的乘客都只能乘这一辆大巴,十分钟过去后,不管后面还有多少没挤上车的乘客,这班车都必须发走。
在这个故事里,“司机” 就是我们的节流阀,他控制发车的时机;“乘客”就是因为我们频繁操作事件而不断涌入的回调任务,它需要接受“司机”的安排;而“计时器”,就是我们上文提到的以自由变量形式存在的时间信息,它是“司机”决定发车的依据;最后“发车”这个动作,就对应到回调函数的执行。
总结下来,所谓的“节流”,是通过在一段时间内无视后来产生的回调请求来实现的。只要一位客人叫了车,司机就会为他开启计时器,一定的时间内,后面需要乘车的客人都得排队上这一辆车,谁也无法叫到更多的车。
对应到实际的交互上是一样一样的:每当用户触发了一次 scroll 事件,我们就为这个触发操作开启计时器。一段时间内,后续所有的 scroll 事件都会被当作“一辆车的乘客”——它们无法触发新的 scroll 回调。直到“一段时间”到了,第一次触发的 scroll 事件对应的回调才会执行,而“一段时间内”触发的后续的 scroll 回调都会被节流阀无视掉。
1.1 手写节流 Throttle
<body>
<p>throttle</p>
<div id="div1" draggable="true" style="width: 100px; height: 50px; background-color: #ccc; padding: 10px;">
可拖拽
</div>
<script>
function throttle(fn, delay = 100) {
let timer = 0
return function () {
if (timer) return
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = 0
}, delay)
}
}
const div1 = document.getElementById('div1')
div1.addEventListener('drag', throttle((e) => {
console.log('鼠标的位置', e.offsetX, e.offsetY)
}))
</script>
</body>
1.2 应用场景
- 鼠标不断点击触发,mousedown(单位时间内只触发一次)
- 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
二. Debounce: 最后一个人说了算
防抖,即防止抖动。抖动着就先不管它,等啥时候静止了,再做操作。
在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
例如,一个搜索输入框,等输入停止之后,自动执行搜索。
debounce 的问题在于它“太有耐心了”。
试想,如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。
频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。
为了避免弄巧成拙,我们需要借力 throttle 的思想,打造一个“有底线”的 debounce——等你可以,但我有我的原则:delay 时间内,我可以为你重新生成定时器;但只要delay的时间到了,我必须要给用户一个响应。
这个 throttle 与 debounce “合体”思路,已经被很多成熟的前端库应用到了它们的加强版 throttle 函数的实现中
2.1 手写防抖 Debounce
<body>
<p>debounce</p>
搜索 <input id="input1">
<script>
function debounce(fn, delay = 200) {
let timer = 0
return function () {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arguments) // 透传 this 和参数
timer = 0
}, delay)
}
}
const input1 = document.getElementById('input1')
input1.addEventListener('keyup', debounce(() => {
console.log('发起搜索', input1.value)
}), 300)
</script>
</body>
2.2 应用场景
- search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
- window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
三、防抖和节流写法区别
function throttle(fn, delay = 100) {
let timer = 0
return function () {
// 节流直接 return
if (timer) return
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = 0
}, delay)
}
}
function debounce(fn, delay = 200) {
let timer = 0
return function () {
// 防抖 清楚定时器,重新计时
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arguments) // 透传 this 和参数
timer = 0
}, delay)
}
}
四、总结
- 节流: 限制执行频率,有节奏的执行
- 防抖:限制执行次数,多次密集的处罚只执行最后一次
节流关注“过程”
防抖关注“结果”
实际工作中可以使用
lodashwww.lodashjs.com