🚨【关注】专题,快人一步了解本专题最新内容 👉一键关注👈
本文收录于【技术新风向】专题,了解专题中更多精彩内容,欢迎点击了解>>
嗨,亲爱的开发者们,欢迎来到本期非同不一般的技术新风向
解密:神秘丢失的冒泡事件
而这要从,我们发现了一个疑似抖音小程序组件bug说起......
如图所示,这是一个交互上复刻了“抖音视频流”的小程序,用户可以像刷抖音一样,不停地往下滑动,观看新的视频。
而这个小程序,在测试的过程中,会偶现视频在滑动切换过程中卡在这条分界线的情况。
在一边排查一边探究《如何在小程内实现类似抖音的视频滚动播放》的过程中
却!意外层层解密了神秘丢失的冒泡事件
并在反复测试中,跑出完整代码得出了最佳实践案例
🔎 重要线索概览
v1. 排查|swiper + video 组件简单实现视频滚动播放
小程序内如何实现类似于抖音的滚动视频播放呢?熟悉小程序的同学自然会联想到利用 swiper + video 组件的实现途径,这的确是实现滚动视频播放的有效途径。但考虑到每个视频都需发起网络请求,如果在 swiper 组件中直接渲染出所有的 video 组件,必然会引起性能问题。
因此我们在每个 swiper-item 默认渲染视频的封面图片,当滑动到对应视频封面后才开始加载相关视频,滑动离开后将视频销毁。让我们简单实现一个 demo 看下播放效果。
// ttml 代码示例
<swiper
vertical="{{true}}"
circular="{{true}}"
current="{{currSwiperIdx}}"
style="height:{{windowHeight}}px"
bindchange="swiperChange">
<block tt:for="{{imgArr}}">
<swiper-item bindtouchmove="move" bindtouchstart="start" bindtouchend="end">
<view class="main" style="height:{{windowHeight}}px;">
<block tt:if="{{showMediaPosterBg || index !== currIdx}}">
<image style="width:100%; height:100%;" src="{{item}}" mode="" />
</block>
<block tt:elif="{{isLoadFinish}}">
<video
class="video"
style="height:{{windowHeight}}px;"
src="{{urlArray[index]}}"
object-fit="cover"
show-fullscreen-btn="{{false}}"
show-play-btn="{{false}}"
controls="{{false}}"
autoplay="{{true}}"
bindplay="playerPlay">
<image tt:if="{{showVideoImgBg}}" class="absolute_fix" src="{{item}}" mode="" />
</video>
</block>
</view>
</swiper-item>
</block>
</swiper>
完整代码链接:https://microapp.bytedance.com/ide/minicode/2LqVL5G
聪明的开发者可以发现 swiper + video 组件的形式确实可以实现视频的滚动播放,但是在滚动过程中 swiper 组件会出现滑动动画中断。
v2.探索|swiper 组件滑动动画中断问题
使用过 swiper 组件的同学应该知道,swiper 组件具有自己的动画效果,在滑动过程中,swiper 组件会根据你最后滑动停留的位置确定是否滚动到下一个 swiper-item。从目前的表现来看,swiper 组件没有触发后续的滚动动画,所以我们猜测是否是因为没有识别到后续的滚动手势造成的卡顿呢?
为了验证这个猜想,我们在 swiper-item 上绑定了三个事件:bindtouchmove="move" ,bindtouchstart="start",bindtouchend="end"。在每个事件触发时,会在 vConsole 输出相应的 log。
通过调试可以看到,正常滚动时绑定在 swiper-item 上的事件都被正常触发,但是在 swiper 组件的滑动动画中断时 bindtouchend="end" 事件并没有被触发。
这和我们之前的猜测一致,说明 swiper 组件的卡顿和没有识别到的手势事件相关,那么又是什么原因造成了 touchend 相关的手势的丢失呢?
v3.解密|神秘丢失的冒泡事件
- 我们知道事件冒泡指的是,事件会从最内层的元素开始发生,一直向上传播,直到最外层祖先<html>。其原理如图所示:
从之前的调试我们看到 swiper-item 上绑定的 bindtouchmove="move" ,bindtouchstart="start" 事件都正常触发,这说明事件是可以正常冒泡的,其中的怪异之处在于 最后的 bindtouchend="end" 相关的冒泡事件丢失了。
仔细检查代码逻辑可以发现,我们在 video 中使用了一个 image 组件作为视频封面,用于避免视频加载完成后的黑屏闪动。
在视频加载完成后,我们会主动销毁该 image 组件。而我们最开始的触摸手势都是发生在这个 image 上的,所以是否是因为 image 组件的销毁造成后续冒泡事件的丢失呢?
v4.测试|是否由 image 组件销毁导致
为了验证猜想,我们复现一个最小 demo,通过 setTimeout 模拟视频加载,setTimeout 中的事件执行时会销毁发生触摸手势的 dom 元素。
// demo 的 ttml
<swiper vertical="{{true}}" style="height:100vh" bindchange="swiperChange" bindanimationfinish="swiperAniFinish" bindtransition="swiperTranstion">
<swiper-item>
<view class="item" bindtouchmove="moveOne" bindtouchstart="startOne" bindtouchend="endOne">
<view tt:if="{{show}}" class="item-one"> page one</view>
</view>
</swiper-item>
<swiper-item>
<view class="item" bindtouchmove="moveTwo" bindtouchstart="startTwo" bindtouchend="endTwo">
<view class="item-two"> page two </view>
</view>
</swiper-item>
</swiper>
// demo 的页面 js
Page({
data: {
show:true,
},
onLoad: function (options) {
},
startOne(){
console.log('---->>>>>startOne');
setTimeout(()=> {
this.setData({
show:false,
})
},1000)
},
endOne(){
console.log('---->>>>>endOne')
},
moveOne() {
console.log('---->>>>>moveOne')
},
})
完整示例代码:https://microapp.bytedance.com/ide/minicode/2jho5sH
从示例中可以看到,当滑动 swiper 组件的过程中,如果开始产生的手势的 dom 被销毁,那么后续的手势事件就不会再触发,同时 swiper 组件出现滑动动画中断。
v5.延展|这是抖音小程序特有的情况嘛?
进而产生了疑问,这是抖音小程序特有的情况还是浏览器事件机制本是如此设计呢?话不多说,我们直接上浏览器上写个最小 demo 看看具体情况。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>test</title>
</head>
<body>
<div id="target">
<div id="inner" style="background: red; height:400px" >
1221212122121
</div>
</div>
</body>
</body>
<script>
const $target = document.querySelector('#target');
const $inner = document.querySelector('#inner');
$target.addEventListener('touchstart', function() {
console.log('touchstart');
setTimeout(()=>{
$inner.remove()
// $inner.setAttribute('style','display: none')
// $inner.setAttribute('style','visibility: hidden')
}, 1000);
});
$target.addEventListener('touchmove', function() {
console.log('touchmove');
});
$target.addEventListener('touchend', function() {
console.log('touchend');
});
</script>
</html>
从 chrome 上的示例可以看出,当触发 touchu 事件时,touchustart 相关的事件立刻被触发,如果在 dom 事件销毁前结束 touch 事件,touchend 相关事件可正常执行,但如果在 dom 销毁后再触发 touchend 事件,相关事件则都不会被执行。
由此我们可以看出,当前 dom 销毁会导致发生在 dom 上的后续冒泡事件的一并销毁。后续查阅 mdn 网站上关于 touch 事 件相关的文档,也证实了我们的猜想:dom 元素如果在触摸过程中被移除,那么这个事件仍然会指向它,因此这个事件也不会冒泡到 window 或 document 对象。由此可见冒泡事件的神秘丢失其实也不神秘,这就是浏览器的事件机制。
v6.实践|如何实现在小程内进行视频滚动播放
更进一步,我测试如果直接使用 dom 的属性将 dom 隐藏,即使其 display 属性的值为 none,或者 visibility 属性的值为 hidden,结果又是怎样呢?
显然如果只是隐藏 dom 元素,那么不会影响后续 touch 事件的冒泡,因此所有事件都会正常执行。
v7.分享|最佳实践案例 & tips
最佳实践案例
根据上面的探索,我们了解到了后续冒泡事件消失的原因,自然要解决 swiper 组件滑动动画中断的方式也变得清晰了,只需要让发生 touch 事件的 dom 元素不被销毁,让后续冒泡事件顺利完成,问题便可迎刃而解。
在我们的案例中,我们在 video 组件中,使用了 image 组件作为视频的封面,在视频加载成功后会对 image 组件进行销毁,并且为了视频的正常展示,销毁或隐藏 image 组件也是必不可少的。综合以上情况,我们考虑在视频加载成功后不销毁 image 组件,改为隐藏该组件,这样便可以让冒泡事件顺利完成。此外我们知道,小程序提供一个 hidden 属于专门用于隐藏小程序的组件,利用 hidden 属性便可以很好的解决我们的问题。
完整示例代码:https://microapp.bytedance.com/ide/minicode/2jmVMdG
Tips ~
不要随意销毁产生 touch 事件的内层 dom 元素,dom 元素如果在触摸过程中被移除,那么这个事件仍然会指向它,因此这个事件也不会冒泡到 window 或 document 对象。