监听弹窗 - 动态优先级
前言
最近接到新手引导的需求中,由于新手引导浮窗可以拖拽到页面任何位置,要求它的优先级必须高于页面元素,但又不能高于其他弹窗蒙层,否则会和其他弹窗共存。因此,需要监听当前是否有弹窗,来实现动态优先级,在有弹窗的时候,让新手引导浮窗优先级动态降低。
那么问题来了,怎么监听当前是否有弹窗弹出呢?
正文
首先我需要知道如何判断当前是否有元素遮盖,我查了下api发现了IntersectionObserver
配置中的trackVisibility
追踪可见性变化,正好符合需求。可以追踪到指定元素是否被其他元素覆盖。用法如下:
this.observe = new IntersectionObserver(() => {
// 当监听的元素被其他元素覆盖或者不可见的时候就会触发
},{
delay: 100, // 多久通知一次变化,默认为100毫秒通知一次,限制最低只能100,为了避免过于消耗性能
trackVisibility: true, // 是否追踪可见性变化, 设置该属性后必须设置delay
})
this.observe.observe(element)
那么问题来了,应该监听哪个元素呢?经过我的试验,监听body没法检测到被其他弹框覆盖,并且,得是完全覆盖,才会触发,遮住元素一半也不会触发。最后我监听了头部导航栏,通常来说,能覆盖头部导航栏的一般都是弹窗。
但是还有问题,我如何得知当前覆盖弹窗的优先级呢?这要求我得先取到这个覆盖的节点。通过查阅发现document.elementFromPoint(x,y)
可以获取指定坐标置顶元素。接以上代码如下:
let header = document.querySelector('.header')
this.observe = new IntersectionObserver(() => {
// 当监听的元素被其他元素覆盖或者不可见的时候就会触发
// 获取头部的顶层元素,之所以写1,1坐标是因为头部本身在最顶上,所以不需要去取具体位置
// 直接取左上角的点即可
let topElement = document.elementFromPoint(1, 1)
// 如果头部是顶层元素,说明没有遮盖,消除设置的zIndex
if(topElement==header){
target.style = ''
return
}
// 执行遮盖的逻辑
// 取得置顶元素,也就是弹窗的样式
let style = getComputedStyle(topElement)
console.log(style.zIndex)
},{
delay: 100, // 多久通知一次变化,默认为100毫秒通知一次,限制最低只能100,为了避免过于消耗性能
trackVisibility: true, // 是否追踪可见性变化, 设置该属性后必须设置delay
})
this.observe.observe(header)
写到这,我产生了个疑问,覆盖头部的就一定是弹窗吗?显然不是,那么我们需要思考弹窗的特征有哪些?
我想了想,弹窗通常都是覆盖全屏,并且是fixed定位,于是我增加以下判断:
// ... 省略以上代码
// 取得置顶元素,也就是弹窗的样式
let style = getComputedStyle(topElement)
let bound = topElement.getBoundingClientRect() // 获取置顶元素布局信息
let isModal = bound.width>=window.innerWidth && bound.height>=window.innerHeight && style.position=='fixed'
if(isModal){
// target 为新手引导,降低他的优先级,使其不会跟弹窗共存
target.style.zIndex = style.zIndex - 1
}
// ... 省略以下代码
现在基本能判断弹窗了,但我在测试的过程中又发现了另一个问题,在其他页面的新手引导,与我写的全局新手引导浮层冲突了,出现了两个引导共存的现象,原因是因为,其他页面的新手引导是用box-shadow
高亮当前元素,发散阴影到全屏,此时头部并没有覆盖元素,所以也没触发监听头部的可见性回调。因为盒阴影不是元素,不算遮盖。
于是问题变了,我还需要判断除了弹窗,是否还有全屏阴影覆盖,此时需要降低我的全局新手引导浮层优先级,使页面的优先展示。为此我想了以下方案做尝试:
- 利用mutationObserver监听所有dom插入删除,属性修改,只要有
box-shadow
属性修改,就降低我的全局新手引导浮层优先级。经过尝试无效,样式类直接应用不会触发mutationObserver
,排除该方案。 - 尝试将弹窗前后的网页生成canvas截图来进行对比,取像素值进行比较,看是否一致,不一致则说明有元素覆盖。试验发现需要至少2s才能生成canvas,性能过低,放弃。
- 退而求其次,阴影这种特殊情况,看见一个加一个,通过阴影的类进行判断。无法覆盖未来增加的阴影类,也无法知道当前项目,有哪些类会产生覆盖全屏的阴影。即便能拿到,也不是最佳方案
最后,我想着,要不然就遍历所有dom,挨个判断是否有全屏阴影得了,内心对这个方案并不认可,只是想尝试一下,因为在我印象中,dom操作都是极其耗性能的,并且还是遍历的所有dom。但意外的发现:
console.time('total time')
document.body.querySelectorAll('*').forEach(el=>{
let style = getComputedStyle(el)
})
console.timeEnd('total time')
结果输出:
> total time: 1.27490234375 ms
看到这,我有点意外,我遍历整个dom,并获取样式,整整1571个节点,只花了1毫秒。
但仔细想想,便明白了,操作dom比较耗性能的地方主要在插入和删除,以及一些css属性影响到浏览器渲染,导致重排重绘,重新生成dom渲染树。我在此处,仅仅是查询节点,并获取样式,并没有对浏览器渲染产生任何影响。甚至可能获取的节点及样式本身浏览器就有缓存,取下内存的数据。并不比普通的循环1571次相差太多性能,完全可以当成普通循环几千次。意味着我可以采用当前方案。代码如下:
this.checkShadowTimer && clearInterval(this.checkShadowTimer)
let timeStart = +new Date()
let nodes = document.body.querySelectorAll('*')
this.isRepeatShadow = false
this.checkShadowTimer = setInterval(()=>{
let timeEnd = +new Date()
// 如果顶层元素不是header 说明有弹框覆盖
let isCover = header && document.elementFromPoint(1, 1) != header
// 如果大于10秒还没检测到全屏引导阴影则可以关闭定时器
if(timeEnd - timeStart > 10*1000){
clearInterval(this.checkShadowTimer)
return
}
// 如果当前被弹窗覆盖全屏,则无需检测是否有全屏阴影, isRepeatShadow确保只设置一次优先级,无需重复遍历设置
if(isCover || this.isRepeatShadow){
return
}
// 遍历所有节点,查找全屏阴影,对性能影响不大
nodes.forEach((el)=>{
let style = getComputedStyle(el)
// 有可见覆盖全屏阴影,尺寸超过2000的
if(
style.boxShadow && style.boxShadow!='none' &&
style.boxShadow.split(' ').some(v=> !isNaN(parseInt(v)) && parseInt(v)>=2000) &&
style.opacity!=0 &&
style.display!='none'){
this.isRepeatShadow = true
// target为全局的新手引导
target.style.zIndex = 100
}
})
}, 100)
结束
至此,结合判断弹窗和阴影的方式,即可实现全局浮层的优先级控制,实现动态优先级,保持优先级始终在弹框和阴影之下 😃