有个需求,让视频消失在视图中时停止播放,遂研究起了元素在视图内的可见性。
技术准备
全新的api
window.IntersectionObserverEntry
可以用于观察元素在视图屏幕区域的可见性,详见MDN。
每当元素与屏幕产生区域交叉时,该函数被触发,并返回交叉区域参数。
滚动监听技术
如果不支持上述的api,就要考虑滚动事件监听实现,每次滚动时就计算元素边缘与视图屏幕的相对关系,以此来判断元素在屏幕上的可视范围。
技术实现
为了实现该功能,你需要做以下工作:
1. window.IntersectionObserverEntry
的兼容性判断
该函数节选自成熟框架的代码片段:
// 检查浏览器是否支持 IntersectionObserverEntry API
export function checkIntersectionObserver() {
const inBrowser = typeof window !== 'undefined' && window !== null
if (
inBrowser &&
'IntersectionObserver' in window &&
'IntersectionObserverEntry' in window &&
'intersectionRatio' in window.IntersectionObserverEntry.prototype
) {
// Minimal polyfill for Edge 15's lack of `isIntersecting`
// See: https://github.com/w3c/IntersectionObserver/issues/211
if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
Object.defineProperty(window.IntersectionObserverEntry.prototype, 'isIntersecting', {
get: function() {
return this.intersectionRatio > 0
}
})
}
return true
}
return false
}
2. 获取元素节点信息
所有的方法,都在最后要拿到元素的布局信息,才能获取到元素的可见性。在使用新api时,回调参数中会有节点信息,但是在scroll监听时,需要我们自己获取:
新api:
new window.IntersectionObserver((entries) => {
console.log(entries[0].boundingClientRect) //节点信息
})
scroll监听:
export function getRect(el: HTMLElement) {
return el.getBoundingClientRect()
}
3. 根据节点信息判断元素可见性
checkInView
函数用于从节点信息中判断出元素可见性。我们甚至可以添加参数,调整元素有多少部分可见(preLoad
上滑消失/出现或者preLoadTop
下滑消失/出现),参数都是比例数字。
export interface CheckInviewOptions {
preLoad: number
preLoadTop: number
}
const defaultCheckInviewOptions: CheckInviewOptions = {
preLoad: 1,
preLoadTop: 0
}
export function checkInView(
rect: ClientRect,
options: CheckInviewOptions = defaultCheckInviewOptions
) {
return (
rect.top < window.innerHeight * options.preLoad &&
rect.bottom > options.preLoadTop &&
rect.left < window.innerWidth * options.preLoad &&
rect.right > 0
)
}
当元素处于隐藏状态时,我们需要注意下,比如,元素的display为none,或者元素的父元素不可见:
isHidden
用于判断函数是否隐藏。这里用到了一个特性offsetParent
,用于判断父元素是否隐藏,详见offsetParent。
export function isHidden(el: HTMLElement) {
const style = window.getComputedStyle(el)
const hidden = style.display === 'none'
// offsetParent returns null in the following situations:
// 1. The element or its parent element has the display property set to none.
// 2. The element has the position property set to fixed
const parentHidden = el.offsetParent === null && style.position !== 'fixed'
return hidden || parentHidden
}
4. 函数节流
像我现在做的这需求,并不需要时时刻刻触发回调函数,只需要在滑动结束暂停即可,可以稍做优化:
/**
* 函数节流方法
* @param {Function} fn 需要进行节流执行的函数
* @param {Number} delay 延时执行的时间
* @param {Number} atleast 至少多长时间触发一次
* @return {Function} 返回节流执行的函数
*/
export function throttle(fn, delay, atleast = 0) {
let timer = null // 定时器
let previous = 0 // 记录上一次执行的时间
return (...args) => {
const now = +new Date() // 当前时间戳
if (!previous) previous = now // 赋值开始时间
if (atleast && now - previous > atleast) {
fn.apply(this, args) // 文章下面有给出该行代码的理解
// 重置上一次开始时间为本次结束时间
previous = now
timer && clearTimeout(timer)
} else {
timer && clearTimeout(timer) // 清除上次定时器
timer = setTimeout(() => {
fn.apply(this, args)
console.log('else')
previous = 0
}, delay)
}
}
}
函数copy于网络~
最终函数
完成大部分的技术函数之后,就可以拼装出最后的结果了,ObserveElementVisible
函数用于监听元素的可见性:
export interface observerObj {
observe: (el: HTMLElement) => any
unobserve: (el: HTMLElement) => any
}
export interface CheckInviewOptions {
preLoad: number
preLoadTop: number
}
export function ObserveElementVisible(
el: HTMLElement,
callback: (visible: boolean) => any,
options: CheckInviewOptions
): observerObj {
let observerObj
const hasObserver = checkIntersectionObserver()
if (hasObserver) {
observerObj = new window.IntersectionObserver((entries) => {
callback(!isHidden(el) && checkInView(entries[0].boundingClientRect, options))
})
} else {
let throttleFn = throttle(callback, 200)
let eventFn = (e: Event) => throttleFn(!isHidden(el) && checkInView(getRect(el), options))
observerObj = {
observe(el: HTMLElement) {
// 立即触发一次
callback(isHidden(el) && checkInView(getRect(el), options))
window.addEventListener('scroll', eventFn)
},
unobserve() {
window.removeEventListener('scroll', eventFn)
}
}
}
observerObj.unobserve(el)
observerObj.observe(el)
return observerObj
}
使用
var visibleObserve = ObserveElementVisible(videoDom, (visible) => {
if (!visible) {
videoDom.pause()
}
})
结束语
偷偷的告诉你,除了ObserveElementVisible
是我自己写的,其他的都是copy,前端的本质就是复制粘贴~~
I’m extremely pleased to discover this website. I wanted to thank you for ones time just for this fantastic read!! I definitely enjoyed every part of it and i also have you bookmarked to see new stuff in your site.
I’m extremely pleased to discover this website. I wanted to thank you for ones time just for this fantastic read!! I absolutely enjoyed every part of it and i also have you bookmarked to see new stuff in your site.
I’m extremely pleased to discover this website. I wanted to thank you for ones time just for this fantastic read!! I absolutely enjoyed every part of it and i also have you bookmarked to see new stuff in your site.
Definitely, what a great blog and revealing posts, I definitely will bookmark your site. Best Regards!
I got what you intend,bookmarked, very decent website.
I am incessantly thought about this, thanks for posting.