广告平台通常按照曝光量向广告主收费,但曝光不能简单等同于页面加载。只有广告真正进入用户可视区域,并且停留一定时间,才构成一次有效曝光。使用 jQuery 结合 Region Observer API(浏览器原生实现为 IntersectionObserver)可以精准判断广告与视口的交叉状态,从而为计费提供可靠依据。

一、Region Observer API 的基本原理与广告可视判断
Region Observer 在浏览器中的标准名称是 IntersectionObserver,它提供了一种异步检测目标元素与祖先元素或视口交叉状态的能力。与基于 scroll 事件的监听不同,观察器由浏览器合成器直接调度回调,不需要在滚动过程中反复计算元素位置,因此性能开销更小。创建观察器时需要传入两个核心参数:回调函数和配置对象。配置对象中的 root 表示交叉参照的根容器,默认值为 null,即以当前视口作为参照;threshold 是一个包含若干阈值的数组,用来决定回调在哪些交叉比例节点触发;rootMargin 则可以扩展或缩小根容器边界,类似于 CSS margin 的写法。
对于广告可视区域计费场景,关键属性是回调中每个 entry 的 intersectionRatio。它表示目标元素与根容器交叉部分占目标元素总面积的比例,取值范围在 0 到 1 之间。例如设置阈值为 [0, 0.5, 1] 时,广告从完全不可见逐渐进入视口,会在可见比例达到 0.5 和 1 时分别触发回调。开发者可以根据业务口径选择有效曝光的比例门槛,一般建议将 0.5 作为最低可见标准。如果没有该观察器,就需要在滚动事件中手动计算每个广告元素的 getBoundingClientRect 结果,再与视口高度比较,既容易抖动也消耗主线程资源。
代码示例展示了原生 API 的基本用法:
const adObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('广告可见比例:', entry.intersectionRatio);
}
});
}, {
root: null,
threshold: [0, 0.5, 1],
rootMargin: '0px'
});
document.querySelectorAll('.ad-slot').forEach(ad => adObserver.observe(ad));
上面这段代码能够监听页面上所有 .ad-slot 容器,但它没有解决状态记录、批量初始化和 jQuery 项目中的统一封装问题,这些正是下一部分要处理的内容。
二、用 jQuery 封装可复用的区域观察插件
在大量使用 jQuery 的项目里,直接操作原生观察器会让代码变得分散,尤其当广告位由模板动态渲染时,需要反复调用 observe 方法。封装成一个 jQuery 插件可以保持原有链式调用风格,同时把阈值、最短曝光时长、回调处理等逻辑集中管理。插件内部仍然使用原生 IntersectionObserver 完成监听,只是将 DOM 元素包装成 jQuery 对象后再触发业务回调,这样既保留了性能优势,也降低了使用门槛。
一个完整的插件封装需要考虑以下几个细节:每个被观察元素都需要独立的可见状态,不能因为一个广告进入视口就影响其他广告;观察器回调是异步执行的,业务代码不能依赖回调执行顺序;插件需要返回 this 以支持链式操作。下面的实现通过 $.data 为每个元素保存 visible 和 visibleStart 状态,并在可见与隐藏切换时计算持续时间,从而为计费逻辑提供基础数据。
(function($) {
$.fn.regionObserver = function(options) {
var settings = $.extend({
threshold: 0.5,
rootMargin: '0px',
minVisibleTime: 1000,
onVisible: function() {},
onHidden: function() {}
}, options);
var observer = null;
if ('IntersectionObserver' in window) {
observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
var $ad = $(entry.target);
var data = $ad.data('regionObserver');
if (!data) { return; }
if (entry.isIntersecting && entry.intersectionRatio >= settings.threshold) {
if (!data.visible) {
data.visible = true;
data.visibleStart = Date.now();
settings.onVisible($ad, entry.intersectionRatio, data.visibleStart);
}
} else if (data.visible) {
var visibleDuration = Date.now() - data.visibleStart;
if (visibleDuration >= settings.minVisibleTime) {
settings.onHidden($ad, visibleDuration);
}
data.visible = false;
}
});
}, {
threshold: [0, settings.threshold],
rootMargin: settings.rootMargin
});
}
return this.each(function() {
var $this = $(this);
$this.data('regionObserver', { visible: false, visibleStart: 0 });
if (observer) {
observer.observe(this);
}
});
};
})(jQuery);
插件默认的 threshold 为 0.5,表示广告至少有一半面积进入视口才认为可见。最小曝光时长 minVisibleTime 默认 1000 毫秒,只有隐藏时记录到的可见时间大于该值才会触发 onHidden。业务方可以覆写这两个参数来适配不同广告位的要求。另外,如果浏览器不支持 IntersectionObserver,插件会静默跳过,不执行观察,但业务代码中仍需提供降级方案。
三、广告计费状态机与有效曝光上报
有效的广告曝光并不是一次性动作,而是一个包含可见、计时、隐藏和上报的完整状态流转。广告元素首次进入视口时进入可见状态并记录开始时间;如果它在阈值下方移动、滚出视口或因为 CSS 样式不再展示,则进入隐藏状态;在隐藏时计算与开始时间的差值,只有差值超过最短曝光时长才计为一次有效曝光。如果用户快速滚动页面导致广告一闪而过,持续时间不足,就不应触发计费上报,避免给广告主带来无效扣费。
利用前面的 jQuery 插件,业务代码可以非常简洁地完成状态机和上报工作。例如在 onVisible 中记录曝光开始时间,在 onHidden 中根据时长决定是否调用后端接口。由于回调可能在同一时间触发多个广告,上报请求应当批量或异步发送,防止阻塞主线程。同时需要使用元素自身的 data 属性或全局集合去重,保证同一次曝光不会重复上报。
$('.ad-slot').regionObserver({
threshold: 0.6,
minVisibleTime: 1500,
onVisible: function($el, ratio, startTime) {
$el.attr('data-exposure-id', Date.now());
console.log('广告进入可视区域,比例:', ratio);
},
onHidden: function($el, duration) {
var adId = $el.data('ad-id');
var exposureId = $el.attr('data-exposure-id');
if (adId && exposureId) {
$.post('/api/ad/exposure', {
adId: adId,
exposureId: exposureId,
duration: duration
});
$el.removeAttr('data-exposure-id');
}
}
});
这段示例设置了更高的有效曝光门槛:可见比例达到 0.6 且至少停留 1.5 秒才会上报。值得注意的是,onHidden 触发时已经满足了最短时长,因此直接上报是安全的。如果广告位是通过 Ajax 动态插入的,需要在插入完成后重新调用 $('.ad-slot').regionObserver(...),插件内部的 each 会为新元素创建独立状态并执行 observe。对于已经观察过的元素,插件没有做去重判断,重复调用会产生多个观察器,实际项目可以增加检查逻辑。
四、兼容降级、性能优化与防作弊注意点
虽然现代浏览器普遍支持 IntersectionObserver,但一些旧版本内核或特殊 WebView 仍然缺失。为了保障计费不丢失,可以在插件内部增加降级分支:当 'IntersectionObserver' in window 为假时,转而监听 window 的 scroll 和 resize 事件,用节流函数控制频率,手动计算元素位置与视口交叉比例。降级方案无法完全等同原生观察器的精确阈值,但可以通过 getBoundingClientRect 粗略判断元素是否在视口内,并配合定时器累计曝光时长。
性能方面,原生观察器本身已经足够高效,但如果业务在回调里频繁读写布局属性,仍然可能引发强制同步布局。例如在 onVisible 中调用 $el.offset() 或修改宽高,都会迫使浏览器提前计算样式。正确做法是避免在回调内进行 DOM 写入,需要更新界面时使用 requestAnimationFrame 调度到下一帧。此外,单页应用路由切换时应及时执行 disconnect 或 unobserve,否则观察器会继续持有已移除元素的引用,造成内存泄漏。
防作弊是广告计费系统绕不开的话题。虽然可视区域 API 可以判断元素与视口的交叉情况,但它无法识别广告是否被其他元素遮挡。例如一个绝对定位的透明浮层覆盖在广告上方,广告仍然会被判定为可见。针对这种情况,可以结合 document.elementsFromPoint 采样广告中心点,检查最顶层元素是否仍然属于广告容器。如果广告本身被设置为 display: none 或 visibility: hidden,交叉比例通常为 0,但某些旧实现可能存在差异,因此需要在观察器回调中额外判断元素的宽高是否大于 0。
jQueryRegion Observer API广告可视区域计费修改时间:2026-09-30 03:29:00