在对页面滚动、光标位置移动等事件进行监听的过程中,由用户原因或者其他原因导致的密集操作可能导致严重的性能问题,解决这一问题主要采用函数防抖和函数节流设计。
函数防抖
应用情景
let handle = document.getElementById('handle');
handle.onmousemove = ()=>{
console.log('Hello, world');
}
以上这个代码示例,通过监听 handle 元素上发生的光标移动事件,触发在控制台写入 Hello,world
的动作。在这一过程中,光标每移动一个坐标点,则触发一次动作,如果不加以控制,用户的操作可能轻松触发成百上千次的后续动作。本例是向控制台写入字符串,在实际开发中可能执行的是更为复杂的 DOM 操作,那么过于频繁密集的调用可能会产生性能问题,甚至会使一部分版本低的浏览器发生假死现象。
函数防抖(debounce),是处理短时间内密集动作的一种方式。如果特定时间内存在连续的相同动作,防抖处理之后仅执行其最后一次动作。
代码实现
let body = document.body;
/* 触发的动作函数 */
function myFunc(){
console.log('Hello, world')
}
/* 未经封装的防抖操作 */
body.onmousemove = ()=>{
clearTimeout(myFunc.timer);
myFunc.timer = setTimeout(myFunc,1000);
}
这是一个没有封装的防抖操作,通过设置定时器使本次操作产生一个自定义的延时,在延时期间如果触发同一操作,则清除定时器重新计时。由于该例防抖操作未经封装,可复用性差,需要解决。
/*
* 防抖函数
* @param func
* @param content
*/
function debounce(func,content){
clearTimeout(func.timer);
func.timer = setTimeout(()={
func.call(content);
},1000)
}
body.onmousemove = ()=>{
debounce(myFunc)
}
封装后的防抖函数,接受两个参数,第一个是执行的动作函数,第二个是传入动作函数 call() 方法的参数。需要说明,call() 方法即是 Function.prototype.call()
方法,是 Function 对象的原型对象方法,接受若干参数,其中首个参数接受this 指代,其余参数均为引用方法的函数的参数。同一个函数还可以将自定义延时作为参数传入,形如下例。
/*
* 防抖函数
* @param func
* @param wait
* @param content
*/
function debounce(func,wait,content){
clearTimeout(func.timer);
func.timer = setTimeout(()=>{
func.call(content);
},wait)
}
此例函数基本实现了防抖设计,但也存在明显副作用。debounce 函数设置定时器时,将定时器值赋予公共函数的 timer 属性,这一设计可能导致一些不必要的错误。例如,该函数的首个参数传入一个动作函数,如果该函数为匿名函数,则后续操作中的 clearTimeout()
方法将无法取消先前声明的定时器。
/*
* 防抖函数
* @param func
* @param wait
* @param content
* @returns {function}
*/
function debounce(func,wait) {
let timer = null;
return (content)=>{
clearTimeout(timer);
timer = setTimeout(()=>{
func.call(debounce,content);
},wait)
}
}
function myFunc(text){
body.innerHTML += `<h3>${text}</h3>`;
}
let a = debounce(myFunc,1000);
body.onmousemove = ()=>{
a('Hello, world')
}
本例是采用闭包设计的防抖函数,采用声明包内变量的方式取代了原有的定时器托管,避免了很多因素导致的错误。函数首个参数也不再限定必须接受命名函数,可以接受匿名函数。
函数节流
防抖设计将一系列密集操作整合为最终一次操作,节流与之不同,侧重于在预定的时间间隔有选择的触发这些动作。
应用场景
window.onscroll = ()=>{
console.log('hello world');
};
如上例所示,程序对页面滚动增加了监听,当用户轻拨滚轮时,则触发向控制台写入字符串的动作。上例代码可以实现,但即便用户没有恶意操作,正常的使用依然会大量触发后续动作,由此造成不必要的性能问题。
函数节流(throttle)一般使用定时器法或时间戳法进行实现。简而言之,设定监听定时器,在调用时清空定时器,
代码实现
const body = document.body;
/*
* 节流函数
* @param func
* @param wait
* @param delay
* @returns {function}
*/
function throttle(func, wait, delay) {
let timer = null, args = arguments;
let bTime = new Date(),cTime = null;
return ()=>{
let context = this;
cTime = Date.now();
if(cTime - bTime >= delay){
func.apply(context, args);
bTime = Date.now();
}else {
clearTimeout(timer);
timer = setTimeout(()=>{
func.apply(context, args);
}, wait);
}
}
}
body.onmousemove = throttle(myFun, 50, 500);
节流设计通过相对时间来控制动作的执行,这一点与防抖相同。过程中符合特定条件的情形将被放行,其余清空定时器并重新计时。