在前端开发中,有的页面事件有这样的特点,就是用户不必特地捣乱,他在一个正常的操作中,都有可能在一个短时间内触发多次。如resize,scroll等。如果事件回调中有大量计算,会极速耗费。性能,在用户看起来,页面可能就一时没有响应,这个页面一下子变卡了变慢了。
节流就是一种解决办法。简单地讲,就是让一个函数无法在很短的时间间隔内连续调用,只有当上一次函数执行后过了你规定的时间间隔,才能进行下一次该函数的调用。
函数节流是指一定时间内执行的操作只执行一次,也就是说即预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期,一个比较形象的例子是如果将水龙头拧紧直到水是以水滴的形式流出,那你会发现每隔一段时间,就会有一滴水流出。
function throttle(fn, thresholds = 1000) {
let timeout
let start = new Date()
return function (...args) {
clearTimeout(timeout) // 总是干掉事件回调
let cur = new Date()
if (cur - start >= thresholds) {
fn(...args) // 只执行一部分方法,这些方法是在某个时间段内执行一次
start = cur
} else {
// 让方法在脱离事件后也能执行一次
timeout = setTimeout(function () {
fn(...args)
}, thresholds)
}
}
}
代码是很简单的。如果在react里,对于class组件,最好的方式是将这个方法封装成一个装饰器。
class Foo extends React.Component{
@throttle(160)
handle(){}
}
decorator在TC39的提案有过巨大变动, 目前js里的装饰器有这4种(未来可能加入更多):
- 类的装饰器
- 类访问器的装饰器
- 类属性的装饰器
- 类方法的装饰器
其中类的装饰器使用起来最简单:
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
余下三种装饰器:
function decorator(target, name, descriptor){}
使用装饰器有这样一些规则:
- 通过
descriptor.value
的修改直接给改成不同的值,适用于方法的装饰器。- 通过
descriptor.get
或descriptor.set
修改逻辑,适用于访问器的装饰器。- 通过
descriptor.initializer
修改属性值,适用于属性的装饰器。- 修改
configurable
、writable
、enumerable
控制属性本身的特性,常见的就是修改为只读。
对于节流装饰器,要考虑1,3的情况,因为类的方法可能存在箭头函数(类的属性)。
return function (target, property, descriptor) {
let timeout
let start = new Date()
if (!(descriptor.value || descriptor.initializer)) throw new SyntaxError('Only functions can be throttled')
let oldFn
let newFn = function (...args) {
oldFn = oldFn || (descriptor.value ? descriptor.value.bind(this) : descriptor.initializer.call(this))
clearTimeout(timeout) // 总是干掉事件回调
let cur = new Date()
if (cur - start >= thresholds) {
oldFn(args) // 只执行一部分方法,这些方法是在某个时间段内执行一次
start = cur
} else {
// 让方法在脱离事件后也能执行一次
timeout = setTimeout(function () {
oldFn(args)
}, thresholds)
}
}
if (descriptor.initializer) {
return {
enumerable: false,
configurable: true,
get: function () {
return newFn
},
}
} else {
return {
...descriptor,
value: newFn,
}
}
}
这里其实有个坑点,就是this。
如果是方法装饰器,改写后的方法应该和老方法一致,根据调用者决定,所以bind(this)
。
如果是属性装饰器,也就是箭头函数,箭头函数内的this应该是当前组件。而descriptor.initializer
的调用结果返回的就是方法本身,也就是定义的箭头函数。
“箭头函数”的
this
,总是指向定义时所在的对象,而不是运行时所在的对象。
descriptor.initializer
的打印结果:
initializer() {
return () => {};
}
descriptor.initializer
执行时,箭头函数才在initializer内部被定义,而initializer的this为descriptor,所以此时箭头函数内部的this是descriptor。
//descriptor
{
configurable: true
enumerable: true
initializer: ƒ initializer()
writable: true
}
所以需要在调用initializer时绑定this为当前组件
descriptor.initializer.call(this)
本以为是装饰器的问题,结果是this的问题,看来有必要针对this再写一篇文章了。