vue 与 throttle 的坑

vue 与 throttle 的坑

Lodash 一直是我十分喜欢的一个工具库,其中 throttle 节流函数适用于控制类似 scroll 事件回调这种极其频繁场景上。可惜的是在配合 vue 使用上变得不那么好用,让我觉得非常可惜,当然预先说一句,这实际上不是 throttle ,Lodash 设计的问题,更多的是 vue 的问题。

p7ZAtx.md.jpg

问题一 绑定不便

事实上不只是 throttle ,相当一类工具类函数,特指 Lodash 中那一类接受一个函数返回一个新的函数的函数,会遇到无法直接绑定到 component 的 methods 上,首先是无法使用对象方法的简写语法,即

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default {
methods:{
methods1(){
// this.something
}
// 想啥呢?
throttle(()=>{
}),
methods2:throttle(()=>{
// 没有 this
})
}
}

这样的方法,读取到 vue 挂载 component 的时候为 methods 中的方法绑定的 this,

解决方案

这个问题好解决,本身 methods 用对象方法也只是一个简写,把 function 定义的方法的 this 指向当前组件实例。所以解决方法很简单

1
2
3
4
5
6
7
8
export default {
methods:{
methods3:throttle(function(){
// 可以正常使用 this
})
}
}

这个其实没什么好说的,在官网上面都有说明,只是我没有想到而已。我没有注意到还是因为惯性思维,习惯了用箭头函数跟对象方法的简写定义,忘记直接用 function 才是最基本的做法。

但是要留意的一点是这里可以 throttle 里的 function 可以绑定到组件实例的 this,一个重要的原因是,throttle 返回的包裹函数调用的时候一般会使用 apply 来将 this 传入调用的 function ,没有用 apply 直接调用的话 this 就不是组件实例了 。

类似 Lodash ,都会细心的处理到这一点,如果你自己定义一些类似的工具函数,不要忘记在包裹函数里通过 apply 去调用。

问题二 实例共享

如果说第一个问题还是个小问题,那么第二个问题要烦人许多。这不是单个特例,而同样的是一类特定函数会遇到,其中同样可以以 throttle 作为例子。

让我们回看问题一的解决方法,其中解决了 this 的指向问题,不过,请考虑以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div @click='click' v-text='num'>
</div>
</template>
<script>
export default {
name:'com'
data(){
return {
num: 1
}
},
methods:{
click:throttle(function(){
// 可以正常使用 this
this.num+=1
},2000)
}
}
</script>
// -----------
<template>
<com></com>
<com></com>
</template>
<script>
export default {
name:'parent'
component:{com}
}
</script>

按照预期,我们会希望子组件 com 的点击事件每隔 2s 触发一次,然而在上面的有两个 com 子组件的例子中,假如你在 2s 内分别点击了两个 com 子组件,就会发现只有第一个被点击的 com 子组件正常执行了 num + 1 操作。而第二个被点击的组件无法触发 num + 1 操作,这就是第二问题了。

我们知道 throttle 可以控制函数的调用频率,即使不去看其实现,我们都知道它必然是通过闭包保存了一个计时器,以判断是否应该执行传入的函数。

闭包当然保证了每个 throttle 都是独立的,这点毋庸置疑。问题依然在于 vue 的 methods 的定义方式。

当我们如上例所示定义了一个函数的时候,需要注意的是我们的 throttle 实际上只在定义组件的时候执行了一次,赋值给 click ,因此这个组件的所有实例共享同一个静态的 click 方法,共享了同一个 throttle 返回的闭包计时器。

这就导致了上面的问题,存在多个组件实例时,不是每个实例一个 throttle 计时器,而是共用了一个计时器,相互影响。

从这里可以推论出,凡是形如 throttle 那样借助闭包保存私有变量比如计时的函数,如果用上面的使用方法,都会遇到闭包私有变量被多个组件实例共享的问题

目前来说,只要还是想要将方法都静态定义于 methods 中就没有什么解决方案。

解决方法

所以,如果你想要避免这个问题,那就只能考虑在组件实例化的时候才去定义这些方法了。

你可以在mounted及之前的生命周期中,又或者是 data 函数中去定义这些方法

1
2
3
4
5
6
7
8
9
10
11
12
export default {
data(){
const click = throttle(()=>this.num+ =1, 1000)
return {
click,
}
},
mounted(){
// 一个额外的好处是可以使用箭头函数
this.click = throttle(()=>this.num+ =1, 1000)
}
}

不过,这样就失去了在 methods 定义函数的直观这一点,其次是在这两者中定义的时候,需要考虑到生命周期的问题,是否在定义的时候会使用到一些还未初始化的数据。

比如这样就不行

1
2
3
4
5
6
7
export default {
created(){
// 此时 $el 未被初始化
const name = this.$el.name
this[name] = throttle(()=>this.num+ =1, 1000)
}
}

总结

总的来说,本文展示了 vue component 的 methods 定义方式下的两个问题,分别是通过函数返回函数时,函数中 this 指向问题,以及通过函数返回函数时,闭包私用变量被共享的问题

第一个问题可以通过 function 关键字解决,第二个问题,如果不涉及到多实例会同时使用的情况,那么就没有问题,不然则只能通过在组件实例化的时候在各个生命周期中或者data函数中定义方法。