开头的废话
首先,本人也不是什么大佬,只是有感兴趣的会自己写写画画。所以,大佬们有什么建议可以互相交流,但求轻喷,哈哈哈!
很惭愧,工作也有几个年头了。刚工作时,在CSDN也写过几篇学习笔记的博客,后来对博客也就没啥兴趣了。个人的观念是学习是自己的事情,不必开着博客到处展览。但换了公司和现在的领导交流之后,才知道自己的想法有些错误。写博客的目的在于分享和交流,甚至能够判断自己的技术方向是否有偏离,如果能结交一些志同道合的朋友,那就是更大的收获了(本人性格外冷内逗,欢迎交流)。
废话一大堆,下面开始一本正经的聊聊我写的bug,咳咳咳(严肃)!
1、节流函数
节流函数是指在高频率的调用过程中,需要稀释函数或方法的执行频率。在指定时间内,只调用一次指定的函数或方法。
简单版
var timer = null;
function throttled(func, params, delay) {
if(timer == null) {
timer = setTimeout(() => {
func(params);
timer = null;
}, delay)
}
}
// 调用
document.body.addEventListener('mousemove', function(event) {
throttled((e) => {
console.log(`事件类型:${e.type},发生时间:${e.timeStamp}`);
}, event, 1000);
});
// 输出结果
// 事件类型:mousemove,发生时间:479.9099999945611
// 事件类型:mousemove,发生时间:1480.7299999520183
// 事件类型:mousemove,发生时间:2481.4400000032037
从执行结果来看,每秒输出一次事件的信息,这也达到了我们的目的。当然,这只是测试,这里日志输出的逻辑可以替换成任何实际有意义的逻辑。但仔细想想,这个节流函数有许多缺点:
- 我们定义了一个全局变量
timer。假设这段代码是引用的第三方库,而我在这个脚本后面的代码中,也定义了timer全局变量,并且设置为初始值为1。那么,在执行throttled函数的时候,timer永远也不为空,于是随后的逻辑也就不会再执行。 - 函数
throttled只接受第二个参数作为func函数的参数。那么,如果我们需要传入多个参数,上面的代码也无法完成。 - 假设在首次调用函数
throttled时,通过setTimeout设置了定时器,直到定时器执行之前,timer都不再为空。那么,定时器的回调函数取到的func参数和params参数永远都是首次调用throttled函数所设置的参数。如果需要更新参数,这种写法也是无法办到的。
闭包版
为了解决上面的三个问题,我们可以考虑使用闭包。
function throttle(delay) {
var timer, lastFunc, lastParams;
return function(func) {
lastFunc = func;
lastParams = Array.prototype.slice.call(arguments, 1);
if(timer == null) {
timer = setTimeout(() => {
lastFunc.apply(this, lastParams);
timer = null;
}, delay)
}
}
}
// 调用
var throttled = throttle(1000);
document.body.addEventListener('mousemove', function (event) {
throttled((type, timeStamp, x, y) => {
console.log(`事件类型:${type},发生时间:${timeStamp},坐标:${x},${y}`);
}, event.type, event.timeStamp, event.x, event.y);
});
// 执行结果
// 事件类型:mousemove,发生时间:1154.0700000477955,坐标:194,151
// 事件类型:mousemove,发生时间:2155.3149999817833,坐标:274,208
从上面的执行结果来看,在简单版中遇到的三个问题,这里统统得到了解决:
- 全局变量被删除了,取而代之的是函数内的局部变量。
- 支持可变的参数,通过
arguments变量获得实际的参数。arguments是类数组对象,通过slice方法将其转换成真正的数组,将这个数组传入apply方法,调用func函数。 - 创建
lastFunc和lastParams这两个额外的局部变量,用来缓存最新的参数。定时器的回调函数执行时,就可以使用最新的参数。
this指针
在上面的代码中,我修改了func函数的调用方式,采用func.apply(this, lastParams)的方式来调用。这么做的原因是为了修改调用func函数的this指针。让我们来测试两种调用方式的区别,如下是一段简化的代码:
function throttle(func, delay) {
var timer;
return function() {
if(timer == null) {
timer = setTimeout(() => {
// func.apply(this);
// func();
timer = null;
}, delay)
}
}
}
var throttled = throttle(function () {
console.log(this);
}, 1000);
document.body.addEventListener('mousemove', throttled);
首先,解开func()的注释,输出结果如下:
Window {parent: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}
然后,解开func.apply(this)的注释,输出结果如下:
<body>...</body>
假设不使用节流函数,事件回调中的this是什么呢?
document.body.addEventListener('mousemove', function () {
console.log(this);
});
// 执行结果
// <body>...</body>
显然,我们更希望this的指向是第二种结果,因为我们不会希望函数放到节流函数中以后,this的指向就变成了window。
这里先回顾一下this指针指向的规律:
- 全局函数的
this指向window对象(严格模式下,全局函数的this指向undefined)。 - 对象的方法指向调用的对象。
- 匿名函数具有全局性。也就是说,匿名函数的
this也通常指向window。 - 箭头函数没有自己的
this指向,它的this指向是从它的上层作用域继承过来的。
当然,apply和call都可以改变this的指向,而上述只是在不改变this指向的常见情况。
当调用func()这段代码时,这是匿名函数且没有改变它的this指向。因此,它指向全局对象。而调用func.apply(this)修改了this指向之后,新的this指向取决于throttle所返回的throttled闭包函数是如何执行的。而根据结果猜测,这个闭包函数被当作是body对象的方法执行,意味着this指向被修改成了body对象。
指定this指向
在上一节,我们说明了为什么使用apply修改this指针。但这还不够,假设节流函数调用的是某个对象的方法呢?
function throttle(delay) {
var timer, lastFunc, lastParams;
return function(func) {
lastFunc = func;
lastParams = Array.prototype.slice.call(arguments, 1);
if(timer == null) {
timer = setTimeout(() => {
lastFunc.apply(this, lastParams);
timer = null;
}, delay)
}
}
}
// 调用
var obj = {
name: 'obj',
say: function() {
console.log(this.name);
}
}
var name = 'window';
var throttled = throttle(1000);
document.body.addEventListener('mousemove', function (event) {
throttled(obj.say);
});
// 执行结果
// "window"
很明显,因为this的指向错误,导致输出结果也错误了。那么,增加一个参数传入this值。
function throttle(delay) {
var timer, lastFunc, lastThisArg, lastParams;
return function(func, thisArg) {
lastFunc = func;
lastThisArg = thisArg;
lastParams = Array.prototype.slice.call(arguments, 2);
if(timer == null) {
timer = setTimeout(() => {
lastFunc.apply(lastThisArg || this, lastParams);
timer = null;
}, delay)
}
}
}
// 调用
var obj = {
name: 'obj',
say: function() {
console.log(this.name);
}
}
var name = 'window';
var throttled = throttle(1000);
document.body.addEventListener('mousemove', function (event) {
throttled(obj.say, obj);
});
// 执行结果
// "obj"
于是,节流函数就写好了。
2、防抖函数
防抖函数是指某个事件发生之后的指定时间段内,没有再发生该事件,防抖函数将会执行一次。否则,重新计算时间。
有了节流函数的铺垫,防抖函数也非常好写,这里不再做过多的解释,代码如下。
function debounce(delay) {
var timer = null;
return function(func, thisArg) {
var args = Array.prototype.slice.call(arguments, 2);
if(timer) {
window.clearTimeout(timer);
}
timer = setTimeout(()=> {
func.apply(thisArg || this, args);
timer = null;
}, delay);
}
}
// 调用
var debounced = debounce(1000);
document.body.addEventListener('mousemove', function (event) {
debounced((type, timeStamp, x, y) => {
console.log(`事件类型:${type},发生时间:${timeStamp},坐标:${x},${y}`);
}, undefined, event.type, event.timeStamp, event.x, event.y);
});