页面可见性和IntersectionObserver交叉观察器

536 阅读4分钟

在实际开发工作中,判断页面是否可见来执行相关逻辑,能提升用户的体验性,比如页面不可见时,主动暂定正在播放的视频,并在页面可见时,主动播放视频等。

IntersectionObserver:特定的内容是否显示在了视口内,即用户是否看见了,可实际用于用户必须阅读完条文后,才能点击相关按钮或特定的内容添加相关埋点等等

Page Visibility API 页面可见性

某些场景下,开发者需要知道用户是否正在浏览网页内容。

Page Visibility API 可以监听页面的可见性发生变化。Document.visibilityState(只读属性),返回 document 的可见性。

属性值:字符串

  • hidden: 页面彻底不可见 所有浏览器必须支持

    触发场景:

    • 浏览器最小化

    • 浏览器没有最小化,但是当前页面切换成了背景页

    • 操作系统触发锁屏屏幕

  • visible:页面至少一部分可见 所有浏览器必须支持

  • prerender:页面即将或正在渲染,处于不可见状态 只在支持"预渲染"的浏览器上才会出现

visibilityChange 事件

只要 document.visibilityState 属性发生变化,就会触发 visibilityChange 事件

document.addEventListener('visibilitychange', () => {
    console.log( document.visibilityState )
    // 页面可见性发生改变后,执行响应逻辑	
})

通过监听网页的可见性,可以用来节省资源,当用户不看网页时,可以暂停对服务器的轮询、暂停网页动画、暂停播放音频或视频等操作

IntersectionObserver API 交叉观察器

IntersectionObserver 接口提供了一种异步观察目标元素与其祖先元素或视口(viewport)交叉状态的方法。祖先元素或视口(viewport)被称为根(root)

API 使用

1、创建观察者(观察者一旦被创建,其配置则无法更改)

// callback 参数是目标元素与root交叉变化时的回调函数
// option 参数是配置对象
let observer = new IntersectionObserver(callback, option)

2、开始观察

observer.observe(dom)

3、停止观察指定元素或关闭观察器

// 停止观察指定元素
observe.unobserve(dom)

// 关闭观察器
observe.disconnect()

callback 回调函数 参数

let observer = new IntersectionObserver((entries) => {
    console.log(entries)
})

未在 option 中配置 threshold 值时,callback 针对一个目标元素,一般会触发三次。

  • 开始观察目标元素时,会主动触发一次回调

  • 目标元素刚刚和 root 交叉

  • 目标元素和 root 从交叉到完全不交叉

callback 函数的入参是一个数组,数组成员为 IntersectionObserverEntry 对象

IntersectionObserverEntry 对象: 只读

image.png

  • boundingClientRect: 目标元素的 DOMRect 信息
  • intersectionRatio: 目标元素与 root 的交叉区域占目标元素的比例
  • intersectionRect: 交叉区域的 DOMRect 信息
  • isIntersection: 目标元素与 root 是否交叉
  • rootBoundsrootDOMRect 信息
  • target:目标元素 DOM 节点对象

option 配置对象

  • root :监听元素的具体祖先元素,默认为视口 viewport

  • thresholds:比如:数组 [0.2, 0.8] 。数组内数据升序排列。当 intersectionRatio 超过数组内值时,触发回调,默认为 0

  • rootMargin:计算交叉时添加到根( root )边界盒 bounding box 的矩形偏移量,可以有效的缩小或扩大根的判定范围从而满足计算要求。所有偏移量均可用像素或者百分比来表达,默认值为 "0px 0px 0px 0px"

Vue 自定义指令:IntersectionObserver 配合 Velocity 动画库

// 代码可引入全局自定义指令
/**
TODO: 
    当浏览器不支持交叉观察器时,使用 getBoundingClientRect 配置 scroll 事件来兼容,代码中固定了 scroll container 为 "#app"
    可根据实际需求添加自定义指令的绑定值或者使用 mixin 方式改用局部注册方式实现
*/
import Vue from 'vue'
import Velocity from 'velocity-animate'
import { throttle } from 'lodash'

// 是否支持交叉观察器api
const isIntersectionObserver = 'IntersectionObserver' in window
// viewport 宽高
let vHeight
let vWidth

window.addEventListener('resize', calcWindowInfo)

calcWindowInfo()

// vue 插件
const globalDirective = {
  install (Vue) {
    Vue.directive('observe', {
      bind (el) {
        el.style.opacity = 0
      },
      inserted (el) {
        if (isIntersectionObserver) {
          // 使用交叉观察器
          observe(el)
        } else {
          // el是绑定指令的dom对象,可自由添加属性
          el.$handler = throttleFun(() => {
            calcIntersecting(el, el.$handler)
          }, 300)
          // 监听scroll事件,并结合 getBoundingClientRect 来判断是否显示在viewport
          listenerScroll(el)
        }
      },
      unbind (el) {
        if (el.$handler) {
          document.getElementById('app').removeEventListener('scroll', el.$handler)
        }
      }
    })
  }
}

// 使用交叉观察器
function observe (el) {
  const observer = new IntersectionObserver(entries => {
    if (entries[0].isIntersecting) {
      // 取消观测
      observer.unobserve(el)
      animVelocity(el)
    }
  })
  // 观测
  observer.observe(el)
}

const throttleFun = (func, wait = 50) => throttle(func, wait)

// 监听scroll事件,并通过DomRect判断是否交叉
function listenerScroll (el) {
  // 根据实际情况,给特定的dom添加scroll事件监听
  document.getElementById('app').addEventListener('scroll', el.$handler)
  setTimeout(() => {
    el.$handler()
  }, 60)
}

function calcIntersecting (el, handler) {
  const { top, right, bottom, left } = el.getBoundingClientRect()
  // 在viewport内
  if (!(bottom < 0 || left > vWidth || top > vHeight || right < 0)) {
    // 移除监听
    document.getElementById('app').removeEventListener('scroll', handler)
    animVelocity(el)
  }
}

// 执行dom显示动画
function animVelocity (el) {
  Velocity(el, {
    /* translateX 初始值永远为-80px 动画结束值为0 */
    translateX: [0, -80],
    translateY: [0, 50],
    scale: [1, 0.8],
    /* opacity 初始值永远为0 动画结束值为1 缓动效果为"easeInSine" */
    opacity: [1, 'easeInSine', 0]
  })
}

// viewport 宽高信息
function calcWindowInfo () {
  vHeight = document.documentElement.clientHeight || document.body.clientHeight
  vWidth = document.documentElement.clientWidth || document.body.clientWidth
}

Vue.use(globalDirective)

export default globalDirective

参考文章: