跟踪元素可视?试试Intersection Observer

发表于 2年以前  | 总阅读数:408 次

本文将讲解Intersection Observer的用法及其polyfill的原理,我们一起来看下。

背景

现在有以下几种场景。

  1. 页面滚动时懒加载图片
  2. 实现无线滚动页面(Infinite scrolling)
  3. 根据某个元素是否出现在视窗从而执行某些逻辑



对于这些传统的实现方法是,监听到scroll事件后,调用目标元素的getBoundingClientRect()方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll事件是同步事件,在滚动时密集发生,计算量很大,容易造成性能问题。经常需要配合节流一起使用。

这时候 Intersection Observer 就可以优秀的解决我们上述问题。

getBoundingClientRect()(地址:https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)

Intersection Observer概念及用法

Intersection Observer是w3c提出的一种 Observer API,属于浏览器中全局可访问对象,Intersection Observer 能够更好地支持上述场景,因为 Observer 并不在主线程中执行,降低了资源消耗,优化了网页性能。

Intersection Observer为web开发者提供了一种异步查询元素相对于其他元素或窗口位置的能力。它常应用于解决追踪一个元素在窗口的可视问题。

注:一旦 IntersectionObserver 被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;但是,你可以在同一个观察者对象中配置监听多个目标元素。

API


const observer = new IntersectionObserver(callback[, options]);
// 方法
// 开始观察某个目标元素 
observer.observe(target) 
// 停止观察某个目标元素
observer.unobserve(target) 
// 关闭监视器 
observer.disconnect() 
// 获取所有 IntersectionObserver 观察的 targets 
observer.takeRecords() 
// 注:该方法是同步获取所有targets,一旦调用,callback回调将不再执行

options 为可选参数。

未指定时,observer实例默认使用文档视口作为root,margin为0,阈值为0%。(即一像素的改变都会触发回调函数)

可以配置的参数有三个:

参数 含义 默认值
root 指定根(root)元素,用于检查目标的可见性。必须是目标元素的父级元素。 默认为顶级文档的视窗。
rootMargin 根(root)元素的外边距,类似于css的margin属性,该属性值是用作 root 元素和 target 发生交集时候的计算交集的区域范围,使用该属性可以控制 root 元素每一边的收缩或者扩张。 默认为"0px 0px 0px 0px"(top, right, bottom, left)。
threshold 类型为number或number组成的数组。该值为 1.0 含义是当 target 完全出现在 root 元素中时候回调才会被执行。[0, 0.25, 0.5, 0.75, 1]就表示目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。 默认值为0。

callback:当元素可见比例超过指定阈值(threshold)后,会调用回调函数,此回调函数接受两个参数:

entries:一个IntersectionObserverEntry对象组成的数组。intersectionObserverEntry提供目标元素的信息,有以下六个属性:

time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒

target:被观察的目标元素,是一个 DOM 节点对象

rootBounds:根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null

boundingClientRect:目标元素的矩形区域的信息

intersectionRect:目标元素与视口(或根元素)的交叉区域的信息

intersectionRatio:目标元素的可见比例,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0

observer:被调用的IntersectionObserver实例。

IntersectionObserver地址:https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver

浏览器兼容性

我们在使用该api时,一定要判断浏览器是否支持,如果不支持,需要我们引入pollify来解决, 我们本篇主要介绍:Intersection Observer polyfill的原理,来了解一下其具体实现。

对Intersection Observer底层源码感兴趣的同学可以看:intersection observe实现



intersection observe实现地址:https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/intersection_observer/intersection_observer.cc

Observe实现原理

observe方法定义在IntersectionObserver原型上


IntersectionObserver.prototype.observe = function(target) {
  var isTargetAlreadyObserved = this._observationTargets.some(function(item) {
    return item.element == target;
  });

  if (isTargetAlreadyObserved) {
    return;
  }

  if (!(target && target.nodeType == 1)) {
    throw new Error('target must be an Element');
  }

  this._registerInstance();
  this._observationTargets.push({element: target, entry: null});
  this._monitorIntersections();
  this._checkForIntersections();
}

该函数接收的参数就是我们需要监测的dom元素(目标元素)。

首先会遍历this._observationTargets数组,这步就是为了判断当前的元素是否已经通过observe方法监测过。如果已经监测过,(isTargetAlreadyObserved为true)就直接return,防止同一个observer实例对同一个target元素进行多次监测。

如果没有监测过target元素,这里会对target的类型进行判断。如果不是一个dom结点(nodeType !== 1),同样会抛出一个错误。

_registerInstance函数做了什么呢?


IntersectionObserver.prototype._registerInstance = function() {
  if (registry.indexOf(this) < 0) {
    registry.push(this);
  }
};

顾名思义,如果我们的observe实例不存在,即将该实例加入到全局registry数组中,避免被垃圾回收机制回收。

_monitorIntersections函数

该函数主要用来实现对目标元素的检测,可以看下具体实现,摘除了一些边界值判断的逻辑,如判断dom已经销毁,判断重复监听等,直接看核心逻辑

IntersectionObserver.prototype._monitorIntersections = function() {
  if (this.POLL_INTERVAL) {
    this._monitoringInterval = setInterval(
        this._checkForIntersections, this.POLL_INTERVAL);
  }
  else {
    addEvent(window, 'resize', this._checkForIntersections, true);
    addEvent(document, 'scroll', this._checkForIntersections, true);

    if (this.USE_MUTATION_OBSERVER && 'MutationObserver' in window) {
      this._domObserver = new MutationObserver(this._checkForIntersections);
      this._domObserver.observe(document, {
        attributes: true,
        childList: true,
        characterData: true,
        subtree: true
      });
    }
  }
}

实现监听的方式有两种

  1. poll_interval:如果设置了轮询时间,则按每隔n秒进行轮询,观察dom变化,这种方式简单粗暴且轮询较耗费性能,因而默认是关闭的。
  2. MutationObserver:这种监听方式是监听窗口的resize和页面的scroll事件,当然,这两种监听满足不了所有的场景,比如:某一个元素的显隐,因而,它使用了是MutationObserve这个api,监听document元素下所有节点的attributeschildListcharacterData的变化,每当有children节点发生变化时都会去检测target元素和root元素的交集状态。

_checkForIntersections函数

上述的_monitorIntersections中有四个地方调用了_checkForIntersection

  1. setInterval轮询监听dom变化时
  2. window的resize
  3. document的scroll
  4. MutationObserver api监听dom变化时作为回调触发

还有就是第一个讲解的observe函数中,作为callback回调触发。该函数的作用是,判断root和target的交集是否发生变化,发生变化则触发observe的回调。


IntersectionObserver.prototype._checkForIntersections = function() {
  if (!this.root && crossOriginUpdater && !crossOriginRect) {
    // Cross origin monitoring, but no initial data available yet.
    return;
  }

  // 判断root是否在dom结构中,传入的root一定要是target的祖先元素
  var rootIsInDom = this._rootIsInDom();
  var rootRect = rootIsInDom ? this._getRootRect() : getEmptyRect();

  this._observationTargets.forEach(function(item) {
    var target = item.element;
    var targetRect = getBoundingClientRect(target);
    var rootContainsTarget = this._rootContainsTarget(target);
    var oldEntry = item.entry;
    var intersectionRect = rootIsInDom && rootContainsTarget &&
        this._computeTargetAndRootIntersection(target, targetRect, rootRect);

    var rootBounds = null;
    if (!this._rootContainsTarget(target)) {
      rootBounds = getEmptyRect();
    } else if (!crossOriginUpdater || this.root) {
      rootBounds = rootRect;
    }

    var newEntry = item.entry = new IntersectionObserverEntry({
      time: now(),
      target: target,
      boundingClientRect: targetRect,
      rootBounds: rootBounds,
      intersectionRect: intersectionRect
    });

    if (!oldEntry) {
      this._queuedEntries.push(newEntry);
    } else if (rootIsInDom && rootContainsTarget) {
      // If the new entry intersection ratio has crossed any of the
      // thresholds, add a new entry.
      if (this._hasCrossedThreshold(oldEntry, newEntry)) {
        this._queuedEntries.push(newEntry);
      }
    } else {
      // If the root is not in the DOM or target is not contained within
      // root but the previous entry for this target had an intersection,
      // add a new record indicating removal.
      if (oldEntry && oldEntry.isIntersecting) {
        this._queuedEntries.push(newEntry);
      }
    }
  }, this);

  if (this._queuedEntries.length) {
    this._callback(this.takeRecords(), this);
  }
};

this._observationTargets这个属性用来保存被observer所监听的所有的target元素。

_getRootRect是获取root元素的区域,这个区域是rootRectrootMargin结合计算出新的rootRect区域的大小。



接着遍历this._observationTargets

在这个forEach遍历中,主要动作就是:搜集root元素和target元素的交集状态,并把他们存入到_queuedEntries数组中。

而计算目标元素和root元素相交区域的核心就是 _computeTargetAndRootIntersection函数

_computeTargetAndRootIntersection函数


IntersectionObserver.prototype._computeTargetAndRootIntersection =
function(target, rootRect) {


  // If the element isn't displayed, an intersection can't happen.
  if (window.getComputedStyle(target).display == 'none') return;


  var targetRect = getBoundingClientRect(target);
  var intersectionRect = targetRect;
  var parent = getParentNode(target);
  // 标志位
  var atRoot = false;


  while (!atRoot) {
    var parentRect = null;
    var parentComputedStyle = parent.nodeType == 1 ?
    window.getComputedStyle(parent) : {};


    // 如果parentRect display为none,target和root元素同样是不可能存在交集的
    if (parentComputedStyle.display == 'none') return;


    if (parent == this.root || parent == document) {
      atRoot = true;
      parentRect = rootRect;
    } else {
      // If the element has a non-visible overflow, and it's not the <body>
      // or <html> element, update the intersection rect.
      // Note: <body> and <html> cannot be clipped to a rect that's not also
      // the document rect, so no need to compute a new intersection.
      if (parent != document.body &&
      parent != document.documentElement &&
      parentComputedStyle.overflow != 'visible') {
        parentRect = getBoundingClientRect(parent);
      }
    }

    // If either of the above conditionals set a new parentRect
    // calculate new intersection data.
    if (parentRect) {
      intersectionRect = computeRectIntersection(parentRect, intersectionRect);
      if (!intersectionRect) break;
    }
      parent = getParentNode(parent);
  }
    return intersectionRect;
}


  function getParentNode(node) {
    var parent = node.parentNode;

    if (parent && parent.nodeType == 11 && parent.host) {
      // If the parent is a shadow root, return the host element.
      return parent.host;
    }

    if (parent && parent.assignedSlot) {
      // If the parent is distributed in a <slot>, return the parent of a slot.
      return parent.assignedSlot.parentNode;
    }  

    return parent;
  }   

‍ 这里判断如果元素是隐藏的,则不可能会相交,直接return。通过atRoot标志位,判断while循环是否循环到了this.root或者是document

如果我们采用默认的root即document,而且parentNode就是document,那么循环将会进入if分支,并将parentRect被赋值为rootRectatRoot设置为true。接着执行第44行代码逻辑。

computeRectIntersection 函数

function computeRectIntersection(rect1, rect2) {
    var top = Math.max(rect1.top, rect2.top);
    var bottom = Math.min(rect1.bottom, rect2.bottom);
    var left = Math.max(rect1.left, rect2.left);
    var right = Math.min(rect1.right, rect2.right);
    var width = right - left;
    var height = bottom - top;


    return (width >= 0 && height >= 0) && {
      top: top,
      bottom: bottom,
      left: left,
      right: right,
      width: width,
      height: height
    };
  }

这里就是在计算两个区域rect1和rect2的交集

红框部分即相交部分的区域~

如果target.parentNode不是document,那么while循环会执行else分支。其中执行else分支有一个条件parentComputedStyle.overflow != 'visible'。如果parentComputedStyle.overflow的值为visible,那么target和root最大的交叉面积就是target的大小。

交叉面积算出来之后,使用IntersectionObserverEntry函数计算出各个属性值

function IntersectionObserverEntry(entry) {
  this.time = entry.time;
  this.target = entry.target;
  this.rootBounds = entry.rootBounds;
  this.boundingClientRect = entry.boundingClientRect;
  this.intersectionRect = entry.intersectionRect || getEmptyRect();
  this.isIntersecting = !!entry.intersectionRect;


  // Calculates the intersection ratio.
  var targetRect = this.boundingClientRect;
  var targetArea = targetRect.width * targetRect.height;
  var intersectionRect = this.intersectionRect;
  var intersectionArea = intersectionRect.width * intersectionRect.height;


// Sets intersection ratio.
if (targetArea) {
  // Round the intersection ratio to avoid floating point math issues:
  // https://github.com/w3c/IntersectionObserver/issues/324
  this.intersectionRatio = Number((intersectionArea / targetArea).toFixed(4));
  } else {
  // If area is zero and is intersecting, sets to 1, otherwise to 0
  this.intersectionRatio = this.isIntersecting ? 1 : 0;
  }
}

然后计算出intersectionRatioisIntersecting的值。

总结到这里,_checkForIntersections函数第11行的遍历完成啦

遍历完成,后面还有两行逻辑~


if (this._queuedEntries.length) {
  this._callback(this.takeRecords(), this);
}

this._queuedEntries 是一个数组,其中每一个元素都是IntersectionObserverEntry实例对象。只有当这个属性的长度大于 0 的时候,才会触发回调函数。

回调函数第一个参数是this.takeRecords()获取到的值,回忆一下上面讲解intersection observe概念的时候,我们说过callback回调的第一个参数entrys,是由IntersectionObserverEntry对象组成的数组,他就是takeRecords方法的返回值,那么takeRecords方法做了什么~


IntersectionObserver.prototype.takeRecords = function() {
  var records = this._queuedEntries.slice();
  this._queuedEntries = [];
  return records;
}

方法实现很简单,使用数组的slice方法对this._queuedEntries进行了一个拷贝,然后清空了this._queuedEntries。我们知道,intersection observe的回调触发和takeRecords的调用都可以用来获取entries(IntersectionObserverEntry对象数组),每个对象的目标元素都包含每次相交的信息,可以显式通过调用takeRecords方法或隐式地通过观察者的回调(oberve的callback第一个参数)自动调用。当我们调用takeRecords后,有一步清空操作,可以看出如果显示调用takeRecords,则callback不会再被调用。

IntersectionObserverEntry地址:https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserverEntry

引用官网的一句话就是:

调用此方法会清除挂起的相交状态列表,因此不会运行回调

以上是处理所有 Observer 的主体逻辑啦。

展望

Intersection Observer, version 2

目前兼容性还不是很好,期待未来征服各主流浏览器

我们不禁要思考,v1版有哪些不足?

Intersection Observer v1 API 可以告诉您元素何时滚动到窗口的视口中,但它不会告诉您该元素是否被任何其他页面内容覆盖(即元素何时被遮挡)或该元素的可视显示已被 transform,opacity有效filter等css属性修改地使其不可见。

Intersection Observer v2 引入了跟踪目标元素的实际“可见性”的概念,就像人类定义的那样。IntersectionObserver通过在构造函数中设置一个选项,相交的IntersectionObserverEntry实例将包含一个名为 isVisible的新布尔字段,isVisible是true,即目标元素完全不被其他内容遮挡,并且没有应用会改变或扭曲其在屏幕上的显示的视觉效果。相反,一个false意味着不能保证。

最后

附上pollify完整源码的github地址:intersection observe pollify源码(地址:https://github.com/GoogleChromeLabs/intersection-observer/blob/main/intersection-observer.js)

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/wMNO5NDcdElb1xr0urw-XQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237226次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8062次阅读
 目录