JavaScript / 前端 · 11月 12, 2020 6

监听元素在视图内的可见性

有个需求,让视频消失在视图中时停止播放,遂研究起了元素在视图内的可见性。

技术准备

全新的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,前端的本质就是复制粘贴~~

冀ICP备19028007号