字节小程序
开发者社区
小程序小游戏
登录
技术新风向丨神秘丢失的冒泡事件-实现在小程序内进行视频滚动播放

技术新风向丨神秘丢失的冒泡事件-实现在小程序内进行视频滚动播放

2444浏览作者: falafala


嗨,亲爱的开发者们,欢迎来到本期非同不一般的技术新风向

解密:神秘丢失的冒泡事件

而这要从,我们发现了一个疑似抖音小程序组件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 对象。



最后一次编辑于 2022年08月18日
加载中