FE-Interview
FE-Interview copied to clipboard
第 9 题:介绍防抖节流原理、区别以及应用,并用JavaScript进行实现
欢迎在下方发表您的优质见解
1)防抖
- 原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
- 适用场景:
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 搜索框联想场景:防止联想发送请求,只发送最后一次输入
- 简易版实现
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
- 立即执行版实现
- 有时希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
// 有时希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
function debounce(func, wait, immediate) {
let timeout;
return function () {
const context = this;
const args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
const callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
} else {
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
}
- 返回值版实现
- func函数可能会有返回值,所以需要返回函数结果,但是当 immediate 为 false 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args) 的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined,所以只在 immediate 为 true 的时候返回函数的执行结果。
function debounce(func, wait, immediate) {
let timeout, result;
return function () {
const context = this;
const args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
const callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
}
else {
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
return result;
}
}
2)节流
- 原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
- 适用场景
- 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
- 缩放场景:监控浏览器resize
- 使用时间戳实现
- 使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
function throttle(func, wait) {
let context, args;
let previous = 0;
return function () {
let now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
- 使用定时器实现
- 当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。
function throttle(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
if (!timeout) {
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
/** 防抖:
* 应用场景:当用户进行了某个行为(例如点击)之后。不希望每次行为都会触发方法,而是行为做出后,一段时间内没有再次重复行为,
* 才给用户响应
* 实现原理 : 每次触发事件时设置一个延时调用方法,并且取消之前的延时调用方法。(每次触发事件时都取消之前的延时调用方法)
* @params fun 传入的防抖函数(callback) delay 等待时间
* */
const debounce = (fun, delay = 500) => {
let timer = null //设定一个定时器
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fun.apply(this, args)
}, delay)
}
}
/** 节流
* 应用场景:用户进行高频事件触发(滚动),但在限制在n秒内只会执行一次。
* 实现原理: 每次触发时间的时候,判断当前是否存在等待执行的延时函数
* @params fun 传入的防抖函数(callback) delay 等待时间
* */
const throttle = (fun, delay = 1000) => {
let flag = true;
return function (...args) {
if (!flag) return;
flag = false
setTimeout(() => {
fun.apply(this, args)
flag = true
}, delay)
}
}
区别:节流不管事件触发多频繁保证在一定时间内一定会执行一次函数。防抖是只在最后一次事件触发后才会执行一次函数
const debouce = (
fn: () => void,
wait: number = 50,
immediate: boolean = true
) => {
let timer: number | null, context: any, args: any;
const later = () =>
setTimeout(() => {
timer = null;
if (!immediate) {
fn.apply(context, args);
}
}, wait);
return function (...params: any[]) {
if (!timer) {
timer = later();
if (immediate) {
fn.apply(this, params);
} else {
context = this;
args = params;
}
} else {
clearTimeout(timer);
timer = later();
}
};
};
//test
window.onmousemove = debouce(console.log, 1000, true);
const createProxyThrottle = (fn,rate){ const lastClick = Date.now() - rate; return new Proxy(fn,{ apply(target,context,args){ if(Date.now() - lastClick >= rate){ fn(args); lastClick = Date.now(); } } }) }
// 1. 防抖:不停的触发事件,只执行最后一次 // 指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次。假如我们设置了一个等待时间 3 秒的函数,在这 3 秒内如果遇到函数调用请求就重新计时 3 秒,直至新的 3 秒内没有函数调用请求,此时执行函数,不然就以此类推重新计时。
function debounce(fn, wait = 500, immediate = false) { let timer = null, result = null; return function (...args) { if (timer) { clearTimeout(timer); } if (immediate && !timer) { result = fn.apply(this, args); } timer = setTimeout(() => { fn.apply(this, args); }, wait); return result; } }
// 2. 函数节流指的是某个函数在一定时间间隔内(例如 3 秒)只执行一次,在这 3 秒内 无视后来产生的函数调用请求,也不会延长时间间隔。3 秒间隔结束后第一次遇到新的函数调用会触发执行,然后在这新的 3 秒内依旧无视后来产生的函数调用请求,以此类推。 // 函数节流非常适用于函数被频繁调用的场景,例如:window.onresize() 事件、mousemove 事件、上传进度等情况。 function throttle(fn, wait = 500, immediate = false) { let timer = null, startTime = Date.now(), result = null; return function (...args) { if(immediate) result = fn.apply(this, args); const now = Date.now(); // 超过了延时时间,马上执行 if(now - startTime > wait) { // 更新开始时间 startTime = Date.now(); result = fn.apply(this, args); }else { // 否则定时指定时间后执行 if(timer) clearTimeout(timer); timer = setTimeout(() => { // 更新开始时间 startTime = Date.now(); fn.apply(this, args); }, wait); } } }
防抖:在n秒时间内,不停的被触发,只执行最后一次 function debounce(fn, n){ let timer = null return function (){ if(timer) { clearTimeout(timer) timer = setTimeout(fn, n) } else { timer = setTimeout(fn, n) } } }
节流:在n秒时间内,不停的被触发,只执行第一次 function throttre (fn, n){ let flag = true return function(){ if(!flag) return flag = false setTimeout(()=>{ fn() flag = true },n) } }
为什么防抖的返回值版本不能直接在函数的调用中直接反返回呢? return func.apply(context, args); 这样不是在不立即执行的时候也可以返回值了嘛
<script type="text/javascript">
let timer;
//防抖函数 我经常用于 防止多次点击 无论点多少下 都只有最后一次才会执行相应逻辑(当然你如果将延时器的时间设置的很短,结果可能会不一样)
function shake(param) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
console.log('防抖')
}, 500)
}
//节流 不论你怎么点 我就按照自己的脾气来(一秒执行一下)
function throttle(param) {
if (timer) return
timer = setTimeout(() => {
console.log('节流')
timer = null;
}, 1000)
}
</script>
const Throttling = (fn, delay) =>{ let last = 0; return function(){ const now = new Date().getTime(); console.log(now-last); if(now-last<delay) return; last = now; return fn(); } }
对
class Debounce {
toggle(callback, wait) {
clearTimeout(this.timeout);
this.timeout = setTimeout(function(){
callback.apply(this, arguments)
}, wait);
}
timeout = null;
}
class Throttle {
toggle(callback) {
for(let e of this.list) {
if(new Date().getTime() - e > 5000) this.list.shift();
}
if(this.list.length < 3) {
this.list.push(new Date().getTime());
callback.call();
}
}
list = [];
}
function debounce(func, wait, immediate) {
var timeout, result
var d = function(){
var context = this
var args = arguments
if(timeout)clearTimeout(timeout)
if(immediate){
var call = !timeout
timeout = setTimeout(function(){
timeout = null
}, wait)
if(call) result = func.apply(context, args)
}else{
timeout = setTimeout(function(){
result = func.apply(context, args)
timeout = null
}, wait)
}
return result
}
d.cancel = function(){
if(timeout) clearTimeout(timeout)
timeout = null
}
return d
}
function throttle(func, wait, options) {
options = options || {}
var timeout, result
var previous = 0
function t(){
var context = this
var args = arguments
var now = +new Date()
previous = options.leading === false && !previous ? now : previous
var r = wait - (now - previous)
if(r > wait || r <= 0){
if(timeout){
clearTimeout(timeout)
timeout = null
}
previous = now
result = func.apply(context, args)
}else if(!timeout && options.trailing !== false){
timeout = setTimeout(function(){
previous = options.leading === false ? 0 : +new Date()
timeout = null
result = func.apply(context, args)
}, r)
}
return result
}
t.cancel = function(){
clearTimeout(timeout)
timeout = null
previous = 0
}
return t
}
// 防抖
const debounce = (func, wait) => {
let timer = null;
return function(){
const context = this;
const args = arguments;
clearTimeout(timer);
setTimeout(() => {
func.apply(context, args);
}, wait)
}
}
// test
const test = debounce(() => { console.log(777); }, 5000);
test('a');
// 节流
const throttle = (func, wait) => {
var flag = true;
return function () {
if (!flag) return;
flag = false;
const context = this;
const args = arguments;
setTimeout(() => {
flag = true;
func.apply(context, args);
}, wait);
}
}
// test
const test = throttle(() => { console.log('111') }, 3000)
setInterval(() => {
test();
}, 1000);
//防抖 function debounce(fn,n){ let timer =null; return function(){ clearTimeout(timer); timer= setTimeout(fn,n); } } let con=debounce(()=>console.log('1'),2000);
//节流 function throttle(fn, n) { let flag = true; return function () { if (flag) { fn(); flag = false; setTimeout(() => (flag = true), n); } }; }
/**
* 防抖节流都是把高频事件导致的高频回调降到低频的处理
*
* 防抖在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时
* 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
* 搜索框联想场景:防止联想发送请求,只发送最后一次输入
*
* 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效
* 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
* 缩放场景:监控浏览器resize
*/
function debounce(fn, timeout) {
let timer;
return function () {
const self = this;
const args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(self, args);
}, timeout);
};
}
function throttle(fn, timeout) {
let timer;
return function () {
const self = this;
const args = arguments;
if (timer) {
return;
}
timer = setTimeout(() => {
fn.apply(self, args);
timer = null;
}, timeout);
};
}
// 以下是测试代码
// const handleInput = debounce((e) => {
// console.log(e);
// }, 1000);
let count = 0;
const handleInput = throttle((e) => {
count++;
console.log(count);
}, 1000);
document.addEventListener("input", handleInput);
防抖(debounce)和节流(throttle)是两种常用的性能优化手段,尤其在处理高频触发的事件(如滚动、窗口大小调整、键盘输入等)时非常有用。
-
防抖(Debounce) 如果一个函数持续被触发,那么在这个函数执行一次之后,只有在一定的时间间隔内没有再次被触发,才会执行第二次。 也就是说,防抖会确保函数在一定时间内只执行一次,即使在这段时间内被触发了很多次。
-
节流(Throttle) 在单位时间,无论触发多少次,函数都只会执行一次。 也就是说,节流会限制函数的执行频率,确保在单位时间内只执行一次。
-
区别
- 防抖注重的是一定时间间隔内只执行一次,而节流注重的是单位时间内只执行一次。
- 防抖是在事件触发后 n 秒内函数只能执行一次,如果在这 n 秒内又触发了这个事件,则重新计算执行时间。 节流是不管事件触发多么频繁,都会保证在 n 秒内只执行一次。
- 如果需要确保用户停止操作一段时间后才执行,可以选择防抖。
- 如果需要确保在一定时间内至少执行一次操作,可以选择节流。
-
应用
-
防抖的主要思想是:在一定时间内,事件处理函数只执行一次,如果在这个时间段内又触发了该事件,则重新计算执行时间。这适用于短时间内的大量触发场景,例如用户连续输入搜索关键字时,浏览器不会立即执行搜索请求,而是等待用户停止输入一段时间后,再执行搜索操作。这样可以避免在用户连续操作时发送过多的无意义请求,提高页面性能和用户体验。
-
而节流的主要思想是:在一段时间内最多触发一次事件,无论这个事件被触发了多少次。每次事件触发时,如果之前的事件还在等待执行,就取消之前的等待,并重新设置等待时间。节流适用于持续的触发场景,确保在一定时间间隔内至少执行一次操作,如监听滚动事件时,每隔一段时间更新页面位置等。
-
总结来说,防抖是将多次执行变为最后一次执行,而节流是将多次执行变成每隔一段时间执行。两者都可以有效减少事件触发的频率,但在不同的使用场景下,选择哪种方法取决于具体需求。如果需要确保用户停止操作一段时间后才执行,可以选择防抖;如果需要确保在一定时间内至少执行一次操作,可以选择节流。
-
防抖实现
function debounce(func, wait) {
// 定义一个变量timeout,用来存储setTimeout返回的定时器的ID。
// 初始值为undefined,表示没有定时器在运行。
let timeout;
// 返回一个函数,这个函数将会代替原始函数func被调用。
return function () {
// 使用context保存当前上下文this,以便在setTimeout中正确调用func。
const context = this;
// 使用arguments对象保存传入的所有参数,以便在setTimeout中传递给func。
const args = arguments;
// 如果timeout存在(即定时器正在运行),则清除它。
// 这样可以确保不会执行旧的定时器回调,只保留最新的。
if (timeout) clearTimeout(timeout);
// 设置一个新的定时器,等待wait毫秒后执行func函数。
// 注意:这里使用了闭包,所以func、context和args的值会在定时器触发时保持不变。
timeout = setTimeout(function () {
// 当定时器触发时,使用apply方法来调用func,并传入之前保存的context和args。
// 这样就可以确保func在正确的上下文中使用正确的参数被调用。
func.apply(context, args);
}, wait);
// 返回的这个函数没有返回值,所以调用它会返回undefined
// 但重要的是,它设置了定时器来调用func,并处理了可能得定时器重叠
};
}
// 用箭头函数和剩余参数的简化版
function debounce(func, wait) {
let timeout;
return function (...args) {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, wait);
};
}
这个防抖函数的工作原理是:
- 当返回的函数被调用时,它首先检查是否已经有定时器在运行(即
timeout
是否存在)。 - 如果存在定时器,它会立即清除该定时器,这样可以防止在
wait
毫秒内多次调用后执行func
。 - 然后,它设置一个新的定时器,等待
wait
毫秒后执行func
。 - 如果在
wait
毫秒内再次调用这个返回的函数,它会再次清除定时器并设置一个新的,确保func
不会在这段时间内执行。 - 只有当
wait
毫秒的等待期内没有再次调用这个返回的函数时,func
才会最终被执行。
这种机制非常适用于处理如输入框实时搜索、窗口大小调整等需要延迟执行的任务,因为这样可以防止函数在短时间内被频繁调用,从而提高性能。
如何使用防抖
要使用上面的防抖函数,需要首先定义一个需要防抖处理的函数,然后将其传递给debounce
函数来获取一个新的防抖函数。
之后,你可以像调用普通函数一样调用这个新的防抖函数。具体使用示例:
/**
* 实时搜索函数
*/
function search(query) {
// 这里可以发送AJAX请求到服务器进行搜索
console.log("Searching for:", query);
// 假设这里模拟一个搜索请求延迟
return new Promise((resolve) =>
setTimeout(() => {
console.log("Search results for:", query);
resolve("Search results");
}, 1000)
);
}
/**
* 使用`debounce`函数创建一个防抖版本的抖索函数`debouncedSearch`
*/
const debouncedSearch = debounce(search, 500);
/**
* 输入框的实时搜索时间处理函数
*/
function handleSearchInput(event) {
const query = event.target.value;
// 使用防抖函数包装搜索函数
debouncedSearch(query);
}
// 获取输入框元素并绑定事件监听器
const searchInput = document.querySelector("#search-input");
searchInput.addEventListener("input", handleSearchInput);
在上面的代码中,首先定义了一个防抖函数debounce
,它接受一个需要防抖的函数func
和一个等待时间wait
作为参数。
防抖函数返回一个新的函数,这个新函数会在每次调用时取消之前的等待定时器,并重新设置一个新的等待定时器。只有在等待时间过后,才会执行原函数func
。
接下来,定义了一个实时搜索函数search
,它接受一个查询参数query
,并打印出搜索内容。在实际应用中,这个函数可能会发送搜索请求到服务器。
然后,使用debounce
函数创建了一个防抖版本的搜索函数debouncedSearch
,并设置防抖间隔为 500 毫秒。这意味着在用户停止输入 500 毫秒后,才会执行搜索操作。
最后,假设有一个搜索框,通过getElementById
获取其 DOM 元素,并监听它的input
事件。在事件处理函数中,使用防抖版本的搜索函数debouncedSearch
来处理用户输入的查询内容。这样,即使用户在输入框中快速输入,也不会立即触发搜索操作,而是会等待用户停止输入一段时间后再执行搜索。
这种方式可以有效减少不必要的搜索请求,提高性能和用户体验。
节流实现
function throttle(func, limit) {
// 定义一个变量,用于标记当前是否处于节流状态
let inThrottle;
// 返回一个新函数,作为节流函数的代理
return function (...args) {
const context = this;
// 检查是否处于节流状态
if (!inThrottle) {
// 调用原始函数,并传入正确的上下文和参数
func.apply(context, args);
// 将节流状态设置为true,表示现在正在处于节流中
inThrottle = true;
// 设置一个定时器,在limit毫秒后将节流状态重置为false
setTimeout(() => (inThrottle = false), limit);
}
// 如果处于节流状态,则不执行任何操作,直接返回
};
}
throttle
函数接受一个需要被节流的函数func
和一个时间间隔limit
作为参数。
它返回一个内部函数,这个内部函数在每次调用时都会检查是否处于节流状态。
如果不处于节流状态,它会调用func
函数,并将节流状态设置为true
,然后设置一个定时器在limit
毫秒后将节流状态重置为false
。
如果处于节流状态,则不会执行func
函数。
通过这种方式,可以确保func
函数在指定的时间间隔内只被调用一次。
如何使用节流
function upadateScrollPosition() {
console.log("Scroll position updated:", window.scrollY);
}
const throttledUpdateScrollPosition = throttle(upadateScrollPosition, 300);
window.addEventListener("scroll", throttledUpdateScrollPosition);
滚动事件监听: 当页面滚动时,可能希望执行某些操作,比如懒加载图片、更新导航栏状态等。 但是,如果每次滚动都触发这些操作,会导致性能问题。 使用节流函数可以确保在一定时间间隔内只执行一次操作。