当你在vue中使用防抖与节流时不知是否遇到这种问题
<template>
<div class="login-container">
<el-button type="primary" class="primary" @click="getUserInfo">登录</el-button>
</div>
</template>
<script>
function debounce(fn,duration=100){
let timer=null
return (...args)=>{
clearTimeout(timer)
timer=setTimeout(()=>{
fn.call(this,...args);
},duration)
}
}
function throttle(fn,duration=100){
let target=true;
return (...arg)=>{
if(!target){
return false;
}
target =false;
setTimeout(()=>{
fn.call(this,...arg);
target=true
},duration)
}
}
export default {
name: "login",
data(){
return{
num:0,
}
},
methods: {
test() {
console.log(`测试防抖,点击了${this.num}次`)
},
getUserInfo() {
const fn = debounce(this.test, 1000);
fn();
this.num+=1;
},
},
}
</script>
结果
找原因,是不是代码问题 进行视口的监听
mounted() {
window.addEventListener("resize",debounce(this.test,1000))
}
结果发现有用,不是代码问题,我就说嘛代码咋会有问题
研究防抖代码发现
-
所谓的防抖函数就是一个闭包;它返回的是一个函数,然后你又去执行这个函数,如何解决闭包问题就是用立即执行函数 debounce返回的是一个函数然后里面执行就形成了闭包。问题就出现在这里
function debounce(fn, duration=100) { let timer = null; return (...arg) => { clearTimeout(timer); timer = setTimeout(() => { fn.call(this,...arg); }, duration) } } const fn = debounce(this.test, 1000); fn();
-
上面的防抖是否是相当于与下面的立即执行函数,由于每次触发点击事件都会返回一个新的匿名函数, 就会生成一个新的函数执行期上下文(称之为执行栈),所以就会防抖失效
(function (fn){ clearTimeout(timer) timer=setTimeout(()=>{ fn(); },1000) })()
那么该如何改呢
<template>
<div class="login-container">
<el-button type="primary" class="primary" @click="getUserInfo">登录</el-button>
</div>
</template>
<script>
import {debounce, throttle} from "../utils/debounce";
export default {
name: "login",
data(){
return{
num:0,
}
},
methods: {
getUserInfo:debounce(()=>{
console.log(`测试防抖`);
}, 1000),
},
}
</script>
所以在vue按钮中想要使用防抖和节流应该直接使用而不是在方法内使用
问题1:vue,this实例问题
对于大佬们提出来的this指向问题,我也试了apply,call方法,没有任何作用,我想到了一种方法,在钩子函数中先把this定义出来,放到全局中,当组件销毁时在进行滞空,具体代码如下,MyDebounce和上面写的没有区别是一样的
<template>
<div class="login-container">
<el-button type="primary" class="primary" @click="getUserInfo">登录</el-button>
</div>
</template>
<script>
function MyDebounce(fn, duration=100) {
let timer = null;
return (...arg) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this,...arg);
}, duration)
}
}
let that=null;
export default {
name: "dealError",
data(){
return{
num:0
}
},
mounted() {
//把vue实例保存到全局中
that=this
},
beforeDestroy() {
//组件销毁前,把定义的滞空
that=null
},
methods:{
test() {
console.log(`测试防抖,点击了${this.num}次`)
},
getUserInfo:MyDebounce(()=>{
console.log(that);
that.num++
that.test()
}, 1000),
}
}
</script>
这样就解决了这个this指向的问题,就可以随意操作了,这个方法简单粗暴,如有问题还请大佬指出
问题2:事件点击的event 和点击参数如何获取
- 当点击没有参数时,只想获取点击事件的event则直接获取就行。如:
<template>
<div class="login-container">
<el-button type="primary" class="primary" @click="getUserInfo">登录</el-button>
</div>
</template>
<script>
function MyDebounce(fn, duration=100) {
let timer = null;
return (...arg) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this,...arg);
}, duration)
}
}
let that=null;
export default {
name: "dealError",
data(){
return{
num:0
}
},
mounted() {
//把vue实例保存到全局中
that=this
},
beforeDestroy() {
//组件销毁前,把定义的滞空
that=null
},
methods:{
test() {
console.log(`测试防抖,点击了${this.num}次`)
},
getUserInfo:MyDebounce((event)=>{
console.log("that对象",that,"点击事件",event);
that.num++
that.test()
}, 1000),
}
}
</script>
- 当有参数时,也想获取事件enent 则可以添加参数,如果只想传参数则就把$event,参数去掉就行了,其他参数照样传递
<template>
<div class="login-container">
<el-button type="primary" class="primary" @click="getUserInfo($event,'23323')">登录</el-button>
</div>
</template>
<script>
import {MyDebounce} from "../utils/MyTools";
let that=null;
export default {
name: "dealError",
data(){
return{
num:0
}
},
mounted() {
//把vue实例保存到全局中
that=this
},
beforeDestroy() {
//组件销毁前,把定义的滞空
that=null
},
methods:{
test() {
console.log(`测试防抖,点击了${this.num}次`)
},
getUserInfo:MyDebounce((event,param)=>{
console.log("that对象",that,"点击事件",event,'参数',param);
that.num++
that.test()
}, 1000),
}
}
</script>
感谢各位看官,小生不才请多指教
2024-06-01,时隔三年才发现问题;注意了!注意了!注意了!重要的事情说三遍,上面的都只是入门级,为什么这么说,说白了就是基础不够扎实,上面的this问题为什么处理不了,我终于弄明白了
重点1:为什么不在方法里面去使用防抖,如下
getUserInfo(){
MyDebounce( ()=> {
console.log(this);
this.num++
this.test()
}, 1000)
},
原因:这样写根本没有任何作用,在vue模板中添加了一个click="getUserInfo"的点击事件,当你点击按钮时,去执行getUserInfo方法,但是该方法里面的MyDebounce防抖函数只是使用了但是没有去执行,怎么执行,可以加立即执行函数或者如上面一样接收返回的函数然后去调用结果是一样的
getUserInfo(){
MyDebounce( ()=> {
console.log(this);
this.num++
this.test()
}, 1000)()
},
加了立即执行函数进行点击你会发现欸嘿有效果,也能获取到vue实例了,是不是觉得没有问题,细心的看官会发现,我连续点击了按钮4次,在设置的时间之内应该直会出现一次打印,所以有问题;
为什么回出现多次打印,防抖没效果?
原因在于在vue模板中添加的点击事件函数执行的是getUserInfo,而不是防抖函数,防抖函数是在getUserInfo内部执行的,你点击多少次按钮都是执行getUserInfo,所以点击多少次,getUserInfo就会重新执行多少次,而其内部的防抖函数其实就跟着执行,点击多少次重新执行,这也就是为什么回打印多次问题;因为外部函数getUserInfo都重新被执行了,内部当然也得重新执行;所以必须把防抖函数变成点击事件的函数体
重点2:如何把防抖函数变成点击事件的函数体?
这里就考考看官在js对象中如何有几种写法?当然是有三种写法,如下
// 写法1
const methods= {
sayHello() {
console.log(this);
}
};
//写法2
const methods={
sayHello:function(){
console.log(this)
}
}
//写法3
const methods= {
sayHello:()=> {
console.log(this);
}
};
在vue中常规的写法就是第一种声明式的普通函数写法,既然第一种普通函数写法不能做到防抖,那么就用第二种函数表达式的方式进行赋值,把防抖函数赋值给getUserInfo变量即可,于是就有了真正的防抖了,第三种箭头函数后面会讲到;【可能有人会问了,你这是给对象中函数的命名方法,vue的methods不一样,你这样问那说明年轻人,Too young Too simple,难道vue的methods不是一个对象吗?】防抖写法如下
getUserInfo: MyDebounce( ()=> {
console.log(this);
this.num++
this.test()
}, 1000),
当你这样使用时你会发现有防抖,但出现了之前上面的问题防抖函数里面没法获取到vue实例,没法操作数据
重点3!!!!!极其重要,这里就会讲到为什么this失效问题
上面讲到函数还有第三中写法,就是箭头函数,箭头函数重要的点在于没有独立的this,并且箭头函数不能改变this指向,箭头函数的this指向的是父级的this如下
const obj = {
a: () => {
console.log(this)//this:window
},
a2:function (){
console.log(this)//this:obj 指向的是obj对象
},
b: {
c:()=> {
console.log(this)//this:window 指向的是window
},
d(){
console.log(this)//this:b 指向的是b对象
}
}
}
知道箭头函数的this指向那我们再来看vue的methods,了解过vue源码的因该知道,vue对方法methods对象的处理方式比较简单,就是循环methods对象的每个属性,然后使用bind方法把vue实例绑定到每个方法中去,所以在vue中我们才能直接使用this.XXX方法;如下简易vue源码
//vue实例对象伪代码
function Vue(options){
// 其他操作...
//对于vue中的methods中的方法进行遍历,把每个方法都放到vue实例中,方便我们使用this.XX方法名字
Object.entries(methods).forEach(([methodName,fn)=>{
this[methodName]=fn.bind(this)
})
}
new Vue(vnode.componentOptions)
在上面提到箭头函数不能改变this指向,如果在vue中定义方法的时候使用了箭头函数进行声明,而vue内部又对methods的this进行改变,这就导致了vue内部无法改变箭头函数的this,无法把方法挂载到vue实例中去,所以vue是不允许在方法中使用箭头函数;看到这里细心的朋友就会发现,我们之前写的防抖函数内部是不是也使用了call函数进行改变this指向
function MyDebounce(fn, duration=100) {
let timer = null;
return (...arg) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this,...arg);
}, duration)
}
}
既然也进行了this指向的改变,那是不是说明你传进来的fn函数不能是箭头函数,那再看看我们传递的函数
getUserInfo: MyDebounce(() => {
console.log(this);
this.num++
this.test()
}, 1000),
很明显我们传递了一个箭头函数,箭头函数无法改变this指向,那么就会取找其父级this,最终结果是没有找到所以this就为undefined【这里为什么是undefined,博主还没有弄明白,看着指向作用域显示的是window,后面又变成了vue实例,最后就变成了undefined,只有在定时器那块执行的时候是undefined,不知道是不是这个定时器的原因,看来还得深造呀】
所以综上所述为什么拿不到this,原因在于防抖函数里面进行了this指向的改变,然而传入的函数使用了箭头函数,无法进行this绑定更换,正确的做法就是不能用箭头函数直接使用普通函数,如下
getUserInfo: MyDebounce(function () {
console.log(this);
this.num++
this.test()
}, 1000),
使用普通函数后就是正常的可以获取this,而且还是vue实例,效果如下
到这里vue中防抖基本如何使用基本解释清楚了,博主在防抖的函数中加了一个可以取消的操作,如下
function MyDebounce(fn, duration = 100) {
let timer = null;
function _executor(...arg) {
clearTimeout(timer);
timer = setTimeout( ()=> {
fn.call(this, ...arg);
}, duration)
}
//取消执行
_executor.cancel = () => {
clearTimeout(timer);
}
return _executor
}
我只要在函数中调用该方法就能进行取消执行,如下
//取消执行
cancel() {
this.getUserInfo.cancel()
}
想象很美好,显示很骨感,不行还报错了,为什么?打印一下发现没有这个取消的方法
重点来了为什么?因为vue源码中对methods的处理使用了原生的bind方法进行vue实例绑定,而原生的bind不会把方法的属性也复制过去,如下
这里就是类似的效果,原生的bind方法生成的函数无法复制原方法的属性,那要如何做呢?我们直接把这个getUserInfo的函数提取到data里面去,反正都是函数,data是没有进行bind处理的,如下
你以为正常了,那可就错了,都说data没有使用bind,那么我们的防抖函数使用的是普通函数,this指向是不是有问题了,cancel函数是显示了,但是this,则变成了null,如下
如何改那就简单了,既然没有使用bind进行绑定,那在data里面的this指向的就是vue实例,那么我们用箭头函数,箭头函数的this会找父级的找到了data,发现其this是vue实例,所以这个时候的this就是vue实例,如下
<template>
<div class="login-container">
<el-button class="primary" type="primary" @click="getUserInfo">登录</el-button>
<el-button class="primary" type="primary" @click="cancel">取消执行</el-button>
</div>
</template>
<script>
function MyDebounce(fn, duration = 100) {
let timer = null;
function _executor(...arg) {
clearTimeout(timer);
timer = setTimeout( ()=> {
fn.call(this, ...arg);
}, duration)
}
//取消执行
_executor.cancel = () => {
clearTimeout(timer);
}
return _executor
}
export default {
name: "login",
data() {
return {
num: 0,
getUserInfo: MyDebounce( ()=>{
console.log(this);
this.num++
this.test()
}, 1000),
}
},
methods: {
test() {
console.log(`测试防抖,点击了${this.num}次`)
},
//取消执行
cancel() {
console.log("打印cancel方法",this.getUserInfo.cancel);
this.getUserInfo.cancel()
}
},
}
</script>
<style>
* {
margin: 0;
padding: 0;
}
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
</style>