你的v-debounce封装真的没问题么?自定义防抖指令踩坑记录
起因
那本是一个闲适的清晨,突然测试的一条信息引起了我的注意。
“你的上线按钮怎么执行的下线操作?”, 我:“???”,
赶紧打开代码一看,没毛病啊,事件绑定的方法没错,提示语也没有打错,发生了啥?
项目跑起来一试,也没问题啊,测试你坑我吧。
跑到测试座位开始对线,一看,还真有问题,咋我自己那就没问题。
赶紧对了下线上版本,也没毛病啊,这还能是偶现bug?
直到我点进了另一个任务,这两任务按钮正好一个是上线一个是下线,竟然真有问题(→_→)
测试大哥,我错了,先别提我研究下。
问题复现
内部代码不方便直接放出来,写了个简单的demo,更好看懂。
首先是一个简单的页面结构
<template>
<div class="container">
<button v-debounce="handlerClick1" v-if="showbutton">1</button>
<button v-debounce="handlerClick2" v-else>2</button>
<button @click="toggle">toggle</button>
</div>
</template>
<script>
export default {
data() {
return {
showbutton: true,
}
},
methods: {
toggle() {
this.showbutton = !this.showbutton
},
handlerClick1() {
console.log(1, this)
},
handlerClick2() {
console.log(2, this)
},
},
}
</script>
这里我就不赘述了,就三个按钮,toggle按钮用来切换其他两个按钮的显示,一个打印1,一个打印2
然后是个简化版的v-debounce
const debounce = (fn) => {
let timer
return () => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn()
}, 300)
}
}
const vDebounce = {
install(Vue) {
Vue.directive('debounce', {
bind(el, binding) {
el.debounceFn = debounce(binding.value)
el.addEventListener('click', el.debounceFn)
},
unbind(el) {
el.removeEventListener('click', el.debounceFn)
},
})
},
}
Vue.use(vDebounce)
应该也没有更简化的了吧,搞定,启动项目!
此时,点击按钮1,控制台输出为
没毛病,然后再点击toggle按钮
点击按钮2,控制台输出为
我的2呢!!!看过vue源码的同学肯定很快就能反应过来,这肯定是patchVnode方法在搞事。
最容易想到的方法就是给每个按钮加上key,你再复用个我看看?
是的,加上key值确实可以解决这个问题,但是你不能要求每个用你这个指令的人,给每个用了这个指令的元素都加上key吧,忘记一次就是一个bug单,害人不浅。所以,来探究下有什么好点的解决方法吧!
vue更新vnode时,怎么处理的自定义指令
vue的diff算法和patch流程,掘金上已经有很多大佬详细讲过了,这里我就不赘述了,我们单独看看updateDirectives做了什么。
// 不管新旧节点,只要一边存在自定义指令,就执行_update
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode);
}
_update包含了各种可能的情况,我们单独看看本次我们问题的情况,也就是新旧节点都存在自定义指令
const dirsWithPostpatch = [];
for (key in newDirs) {
oldDir = oldDirs[key];
dir = newDirs[key];
dir.oldValue = oldDir.value;
dir.oldArg = oldDir.arg;
callHook(dir, "update", vnode, oldVnode);
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir);
}
}
为方便阅读,此处我删了旧节点不存在指令的情况。
简单说,就是vue会把旧的指令参数存在对象里面,然后触发update钩子方法,然后用一个数组存起来
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, "postpatch", () => {
for (let i = 0; i < dirsWithPostpatch.length; i++) {
callHook(dirsWithPostpatch[i], "componentUpdated", vnode, oldVnode);
}
});
}
然后遍历这个数组,分别触发componentUpdated钩子函数。
再然后,再然后就没了(¬‿¬),感情vue就光触发了两钩子。
此时,再回头看一下我们的v-debounce,我们在bind钩子中,给元素绑定了click事件,并且已经生成了防抖的事件执行方法,vue在更新vnode时,并不会重置它,导致了我们遇到的问题。
解决方案
明白问题的起因,解决起来就很简单了,我们在componentUpdated钩子中,重新绑定事件即可。
componentUpdated(el, binding) {
el.removeEventListener('click', el.debounceFn)
el.debounceFn = debounce(binding.value)
el.addEventListener('click', el.debounceFn)
},
控制台执行结果
完全没问题,防抖也正常,下面放下改动后的v-debounce
const debounce = (fn) => {
let timer
return () => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn()
}, 300)
}
}
// 自定义防抖指令
const vDebounce = {
install(Vue) {
Vue.directive('debounce', {
bind(el, binding) {
el.debounceFn = debounce(binding.value)
el.addEventListener('click', el.debounceFn)
},
/* 解决代码 start */
componentUpdated(el, binding) {
el.removeEventListener('click', el.debounceFn)
el.debounceFn = debounce(binding.value)
el.addEventListener('click', el.debounceFn)
},
/* 解决代码 end */
unbind(el) {
el.removeEventListener('click', el.debounceFn)
},
})
},
}
Vue.use(vDebounce)