翻车了,vue3 transition 居然有问题?
1. 前言
最近遇到有用户反馈我们线上网站无法用鼠标滚轮进行滚动的 BUG
,对此,我这个老码农早已经是见怪不怪,坑定是用户自己的浏览器版本太低、操作有问题、用户自己鼠标滚轮坏掉了导致的。
毕竟老夫本地测试网站好好的,怒滚几十次都完全能够流畅的滚动嘛,完全不可能是BUG
呀。
于是在心里怒骂用户太菜了,鼠标滚轮坏了都不知道去修一修,居然还敢提反馈过来
2. 事情大条了
事情的转机出现在我们的测试身上,在上次用户反馈几天后的一个春日的午后,窗边洒满了秒速5厘米粉红的樱花
,老夫的心情也和这樱花一样平和宁静,直到我们的测试妹子冲我怒吼 "TM的,网站滚不动了
"
我 TM
一下就被吓醒了,心里想怎么可能,肯定是测试妹子的鼠标滚轮也 TM
坏掉了,不过好心的我还是要过去帮(接近)测试妹子看看到底啥问题
于是我慢慢踱步到测试妹子旁边,用力滚了滚鼠标滚轮,发现网站确实有滚动条,但是用鼠标滚轮进行滚动后页面居然纹丝不动?
啥情况,我又直接拖动滚动条发现并没有啥问题。
我有点慌了,切到其它网站发现鼠标滚轮居然是正常的,就我们的网站用鼠标滚轮滚动不了了
不会吧,不会吧,居然真的是我完美无瑕、举世无双的代码有问题!!!
3. 找呀找呀找问题
首先排除鼠标滚轮坏掉了,然后排除页面没有滚动条不能滚动,最后就只剩下鼠标滚轮事件被重写了。
于是我定位到滚动元素上的 事件监听表
里面,把 wheel
相关的事件全部 remove
掉之后,发现页面终于能够正常滚动了。
那我感觉问题就比较简单明确了,坑定是有人在 onMounted
中绑定了鼠标滚轮事件,但是在 onUnMounted
中没有删除鼠标滚轮事件。
待老夫查查提交记录,一定要一把揪出这个 害人精
好好批判一番,顺便看看能不能 讹 一杯下午茶
4. 事情也许没有想象的那么简单
当然和你想的一样,事情没有想象的那么简单,毕竟如果那么简单各位看官可能就看不到这篇文章了
我仔细看了看发现事件的绑定在一个第三方库上 vue-cropper
,我心想这么大的第三方库,不可能出现这么大的 BUG
吧,于是我翻看了下 vue-cropper
的源码
kotlin
<div class="vue-cropper" ref="cropper" @mouseover="scaleImg" @mouseout="cancelScale">
</div>
// 缩放图片
scaleImg() {
if (this.canScale) {
window.addEventListener('wheel', this.changeSize, this.passive);
}
},
// 移出框
cancelScale() {
if (this.canScale) {
// 移出鼠标滚轮事件
window.removeEventListener('wheel', this.changeSize);
}
},
unmounted() {
window.removeEventListener("mousemove", this.moveCrop);
window.removeEventListener("mouseup", this.leaveCrop);
window.removeEventListener("touchmove", this.moveCrop);
window.removeEventListener("touchend", this.leaveCrop);
//
this.cancelScale()
}
仔细阅读后,发现没有问题啊,在组件销毁时,会调用 this.cancelScale
解除 wheel
事件的监听。那就是不会出现事件泄露的问题呀,毕竟谁能在组件销毁后,再往 window
上绑定事件呢?
那问题会出在哪里呢?
我又仔细复盘了一下整个过程,发现代码其实没有任何问题,问题出现在用户的操作习惯上!!!
我们的裁切组件放在一个弹窗之中,当用户点击确认按钮后,则弹窗关闭,页面中会放置裁切后的图片。
我发现:
如果用户在对图片裁切之后,点击确认按钮之后,如果鼠标朝着裁切框方向上移动,那么页面就没有办法再用鼠标滚轮进行滚动了
如果用户在对图片裁切之后,点击确认按钮之后,如果鼠标没有朝着裁切框方向上移动,则那么页面就还是正常的。
太奇怪了,我的裁切框明明已经绑定了 @mouseover="scaleImg" @mouseout="cancelScale"
并且在 unmounted
中也进行了 cancelScale
操作,为什么还会出现这种匪夷所思的问题?
5. vue transition 导致问题
反复实验之后,发现导致这个问题的根本原因,竟然是弹窗组件里面的 transition
导致的,被 transition
包裹的组件,居然不会在 unmounted
阶段立即进行销毁,而是要等到动画结束后,才会真正的销毁 DOM
可以看到再生命周期源码 中,如果判断时 transtion 的话,不会立即销毁组件,而是会延迟等待到 transiton
走完之后再销毁组件。
虽然说这样做很正常,毕竟直接销毁后,DOM
节点没有了,怎么体现出动画效果。
但是这样的话由于弹窗在消失过程中的 @mouseover="scaleImg"
mouseover
事件 又会重新触发,导致 wheel
事件又被绑定上了。
这个时候 unMounted
已经被触发完了,而 @mouseout
无法被触发,因为动画执行完毕后 DOM
被销毁了。
然后页面就无法滚动了,因为绑定的 wheel
事件已经被泄露了。
具体可以看 Add inert attribute during Leave transitions 对于这个transiton
导致的事件泄露的讨论
6. 解决问题
知道问题所在,解决的方式就有很多了
- 移出时添加遮罩层解决,这种方式主要会有多余的
DOM
节点,不介意其实问题也不大 - 移出时设置
inert
属性
重点是防止在组件 transition
过程中发生 点击、移入、移出 等等事件的触发即可
7. 结语
我个人感觉 vue
这块可能有设计方面的缺陷,按道理来说 unMounted
应该是能够完全处理所有组件的副作用的,而不是需要通过 hack
的方式来解决这个问题,但是可能由于水平不够,我不太完全了解 vue
这么做的理由。
然后的话就是 BUG
这东西如影随形啊,千算万算没有算到还有这种问题等着你,只是一个正常运行的弹窗还有一个正常运行的裁切组件。放到一起之后,最后导致页面都不能滚动的大 BUG
了