- 产品:我想知道用户到底有没有看到过这个商品(DOM节点)?react
- 开发:这个简单,我监听页面滚动,等商品出如今屏幕中,我就发个 埋点 记录一下。git
- 产品:那你看这种呢,点旁边的按钮这个产品才显示。github
- 开发:那我在 按钮点击事件 里面发 埋点。bash
- 产品:其实咱这还有一种状况,就是。。。服务器
- 开发 :好了!什么都别说了,我 setInterval, 包你满意!dom
- but. 做为开发你能作的,远不止于此。异步
接下来咱们会有三种实现方式,分别是:工具
1. 事件监听ui
2. 定时器spa
3. IntersectionObserver
在开始以前,假设本身有一些工具方法
// 判断dom元素是否在视口中
const isElementInViewPort = (el) => { /* 返回一个布尔值 */ }
// 把数据发送到服务器
const sendLog = (value) => { /**/ }复制代码
最初的想法是使用window.scroll、window.resize进行全局监听。对于特殊状况,好比点击出现的商品,则使用更特殊的处理方式。
注意:对于特殊的商品,要采用特殊的dom结构。所以会比较麻烦。
dom:
// 普通商品
<div data-expose="商品1">...</div>
<div data-expose="商品2">...</div>
// 点击才出现的商品
<div>...</div>
<buttton class="expose-click">显示商品</button>复制代码
js:
// 针对scroll
window.on('scroll', () => {
const elements = document.querySelectorAll('[data-expose]');
const elementList = [...elements];
elementList.forEach((element) => {
// 若是有发送成功的标记,则直接return
if(element.getAttribute('[has-exposed]')) return;
if (isElementInViewPort(element)) {
const logValue = element.getAttribute('[data-expose]')
sendLog(logValue)
// 曝光成功以后,则打一个标记
element.setAttribute('[has-exposed]', "1")
}
})
})
// 针对点击
const target = document.querySelectorAll(".expose-click")
target.on('click', (e) => {
const target = e.target;
if(target.getAttribute('[has-exposed]')) return
const logValue = "点击出现的商品"
sendLog(LogValue)
target.setAttribute('[has-exposed]', "1")
})复制代码
由于方法1中,dom结构不一样,而且方法不够通用,因此思考新的方式。
使用setInterval,则不用考虑元素是怎么出现的,只要出如今了屏幕中,则定时器会自动发现须要曝光的内容。
dom:
<div data-expose="商品1">...</div>
<div data-expose="商品2">...</div>
...
<div data-expose="点击出现的商品">...</div>
<buttton>显示商品</button>复制代码
js:
const observeTimer = setInterval(() => {
const elements = document.querySelectorAll('[data-expose]');
const elementList = [...elements];
elementList.forEach((element) => {
// 若是有发送成功的标记,则直接return
if (element.getAttribute('[has-exposed]')) return;
if (isElementInViewPort(element)) {
const logValue = element.getAttribute('[data-expose]')
sendLog(logValue)
// 曝光成功以后,则打一个标记
element.setAttribute('[has-exposed]', "1")
}
})
}, 500)复制代码
ok. 咱们的终极方案来了 IntersectionObserver。你们能够看到以下这段话:
IntersectionObserver
接口提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。
嗯。 嗯??这不就是专门用来作这个事情的嘛?
而后就看一下兼容性,着实通常。
可是w3c官方提供了polyfill (每一个提案 到 Working Draft阶段一般会提供1-2个polyfill). 而后兼容性就能够说是起飞了。
基本实现以下:
observerLibrary.js
let observer;
const init = (options) => {
//IntersectionObserver接受一个callback, 参数是全部被观察的对象
return observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
let { target } = entry;
// 若是dom在视口中,则发送埋点
if(entry.isEntersecting) {
const logValue = target.getAttribute('data-expose')
sendLog(logValue)
target.setAttribute('has-exposed', "1")
// 中止监听
observer.unobserve(target)
}
})
}, {thresholds: options.thresholds || 0, rootMargin: options.rootMargin || {}})
}
const getObserver = () => observer || init()
const observe = () => getObserver().observe;
const unobserve = () => getObserver().unobserve
const stopObserve = () => {
if(!observer) return;
observer.disconnect()
}
export default {
init,
observe,
unobserve,
stopObserve
}复制代码
myObserver.js
import { init, observe, unobserve, stopObserve } from "observerLibrary"
// 须要一个机会去配置observer, 例如交叉比例达到多大时才算曝光
const options = {}
init(options)
export default {
observe,
unobserve,
stopObserve
}复制代码
component.js
import React from "react"
import { observe, unobserve } from "./myObserver"
const App = (props) => {
return <div>
<div data-expose="商品1" ref={observe}>...</div>
<div data-expose="商品2" ref={observe}>...</div>
</div>
}复制代码
以上的实现应该完成了一个基础的功能,可是会有如下问题:
所以,咱们能够加强一下如今的实现:
最终的observe方式实现以下:
let subjectList = [];
const observe = (el) => {
if(!el) return;
// 过滤掉不存在在document上的节点
subjectList = filterObserveList(subjectList)
const newList = getElementWithExpose(el);
const newItemsNotInOldList = getNewItemsNotInOldList(observeList, newList);
observeList = addNewListToList(observeList, newList);
const observer = getObserver();
newItemsNotInOldList.forEach((item) => {
observer.observe(item);
})
}
/*
* 考虑到dom可能被意外移除,因此考虑当某个target已经不在document.body中的时候,执行unobserve(target)
* */
function filterObserveList(list = []) {
const contains = document.body.contains;
const observer = getViewabilityObserver();
const effectiveObserveList = [];
list.forEach((item, key) => {
if (contains(item)) {
effectiveObserveList.push(item);
} else {
observer.unobserve(item);
}
});
return effectiveObserveList;
};
function getElementWithExpose(el) {
if (!el) return [];
const children = Array.from(el.querySelectorAll(`[data-expose]`));
const isIncludeSelf = !!el.getAttribute('data-expose');
const rawResult = isIncludeSelf ? [...children, el] : children;
return rawResult.filter(item => !item.getAttribute('has-exposed');
};
function getNewItemsNotInOldList(list = [], newList = []) {
return newList.filter(item => !list.includes(item))
};
function addNewListToList(list = [], newList = []) {
return Array.from(new Set([...list, ...newList]))
};复制代码
此外,还有一些其余注意事项,须要使用者去特殊处理。好比:
完整代码能够访问 github。