背景
為了配合項目的一個前端曝光埋點功能堕阔,涉及到列表滾動绒窑,動態(tài)上報曝光行的數(shù)據(jù),進行了一個技術(shù)調(diào)研拷恨。
在前端開發(fā)工作中泣特,常常需要判斷某個元素是否進入了“視口”,一般的做法是監(jiān)聽滾動容器的滾動事件挑随,調(diào)用目標(biāo)元素位置方法状您,一般有兩種方式:
el.offesetTop - document.documentElement.scrollTop <= viewPortHeight
el.getBoundingClientReact().top <= viewPortHeight
第一種:
function isInViewPortOfOne (el) {
// viewPortHeight 兼容所有瀏覽器寫法
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const offsetTop = el.offsetTop
const scrollTop = document.documentElement.scrollTop
const top = offsetTop - scrollTop
console.log('top', top)
// 這里有個+100是為了提前加載+ 100
return top <= viewPortHeight + 100
}
第二種:
function isInViewPortOfTwo (el) {
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const top = el.getBoundingClientRect() && el.getBoundingClientRect().top
console.log('top', top)
return top <= viewPortHeight + 100
}
但是這種會造成頁面的重排勒叠,對性能影響很大。本身社招ATS列表已經(jīng)非常臃腫膏孟,且已經(jīng)有一些性能問題眯分,所以需要尋找性能更好的API來實現(xiàn)。
常見引起重排屬性和方法:
width height margin padding display border position overflow clientWidth clientHeight
clientTop clientLeft offsetWidth offsetHeight offsetTop offsetLeft scrollWidth scrollHeight scrollTop
scrollLeft scrollIntoView() scrollTo() getComputedStyle() getBoundingClientRect() scrollIntoViewIfNeeded()
介紹IntersectionObserver API
目前有一個新的 IntersectionObserver API
柒桑,可以自動"觀察"元素是否可見弊决,Chrome 51+
已經(jīng)支持。由于可見(visible)
的本質(zhì)是魁淳,目標(biāo)元素與視口產(chǎn)生一個交叉區(qū)飘诗,所以這個API
叫做"交叉觀察器"。
API
//初步用法
var io = new IntersectionObserver(callback, option);
IntersectionObserver 是瀏覽器原生提供的構(gòu)造函數(shù)界逛,接受兩個參數(shù): callback是可見性變化時的回調(diào)函數(shù)昆稿,option是配置對象
構(gòu)造函數(shù)的返回值是一個觀察器實例。實例的observe方法可以指定觀察哪個 DOM 節(jié)點息拜。
observe的參數(shù)是一個 DOM 節(jié)點對象溉潭。如果要觀察多個節(jié)點,就要多次調(diào)用這個方法少欺。
// 開始觀察
io.observe(document.getElementById('example'));
// 停止觀察
io.unobserve(element);
// 關(guān)閉觀察器
io.disconnect();
callback
var io = new IntersectionObserver( entries => { console.log(entries); } );
entries
參數(shù)是一個數(shù)組喳瓣,每個被觀察的成員都是一個IntersectionObserverEntry
對象,比如觀察了兩個元素赞别,那么entries
的長度就是2
注意: 第一次調(diào)用new IntersectionObserver
時畏陕,callback
函數(shù)會先調(diào)用一次,即使元素未進入可視區(qū)域仿滔。
IntersectionObserverEntry
IntersectionObserverEntry
對象提供目標(biāo)元素的信息蹭秋,一共有七個屬性
time
:可見性發(fā)生變化的時間,是一個高精度時間戳堤撵,單位為毫秒
target
:被觀察的目標(biāo)元素仁讨,是一個 DOM
節(jié)點對象
rootBounds
:根元素的矩形區(qū)域的信息,getBoundingClientRect()
方法的返回值实昨,如果沒有根元素(即直接相對于視口滾動)洞豁,則返回null
boundingClientRect
:目標(biāo)元素的矩形區(qū)域的信息
intersectionRect
:目標(biāo)元素與視口(或根元素)的交叉區(qū)域的信息
intersectionRatio
:目標(biāo)元素的可見比例,即intersectionRect占boundingClientRect
的比例荒给,完全可見時為1丈挟,完全不可見時小于等于0
isIntersecting
:一個布爾值,指示target
元素是已轉(zhuǎn)換為相交狀態(tài)(true)
還是已脫離相交狀態(tài)(false)
志电。
Option
IntersectionObserver
構(gòu)造函數(shù)的第二個參數(shù)是一個配置對象曙咽。它可以設(shè)置以下屬性。
threshold
threshold
屬性決定了什么時候觸發(fā)回調(diào)函數(shù)挑辆。它是一個數(shù)組例朱,每個成員都是一個門檻值孝情,默認(rèn)為[0]
,即交叉比例(intersectionRatio)
達到0
時(開始出現(xiàn)洒嗤、完全隱藏)觸發(fā)回調(diào)函數(shù)箫荡。
比如threshold
設(shè)置為[0.5]
,這個時候當(dāng)元素第一次進入視口50%
本身的面積的時候會調(diào)用一次渔隶,最后滾動出視口 50%
本身的面積的時候會再調(diào)用一次羔挡。
這個是為啥?
因為沒有取消observer
间唉,如果說只想要調(diào)用一次绞灼,只要在觸發(fā)之后調(diào)用 unobserve
方法取消 observer
就可以了。
new IntersectionObserver(
entries => {/* ... */},
{
threshold: [0, 0.25, 0.5, 0.75, 1]
});
用戶可以自定義這個數(shù)組呈野。比如低矮,[0, 0.25, 0.5, 0.75, 1]
就表示當(dāng)目標(biāo)元素0%、25%际跪、50%商佛、75%喉钢、100%
可見時姆打,會觸發(fā)回調(diào)函數(shù)。
root肠虽、rootMargin
很多時候幔戏,目標(biāo)元素不僅會隨著窗口滾動,還會在容器里面滾動(比如在iframe
窗口里滾動)税课。容器內(nèi)滾動也會影響目標(biāo)元素的可見性
IntersectionObserver API
支持容器內(nèi)滾動闲延。root屬性指定目標(biāo)元素所在的容器節(jié)點(即根元素)。注意韩玩,容器元素必須是目標(biāo)元素的祖先節(jié)點垒玲。
大白話來說,root
其實就是可以設(shè)置以哪個滾動元素為標(biāo)準(zhǔn)找颓,進行視口的計算合愈。
var opts = {
root: document.querySelector('.container'),
rootMargin: "500px 0px"
};
var observer = new IntersectionObserver(
callback,
opts
);
上面代碼中,除了root
屬性击狮,還有rootMargin
屬性佛析。后者定義根元素的margin
,用來擴展或縮小rootBounds
這個矩形的大小彪蓬,從而影響intersectionRect
交叉區(qū)域的大小寸莫。它使用CSS
的定義方法,比如10px 20px 30px 40px
档冬,表示 top膘茎、right桃纯、bottom
和 left
四個方向的值。 這樣設(shè)置以后辽狈,不管是窗口滾動或者容器內(nèi)滾動慈参,只要目標(biāo)元素可見性變化,都會觸發(fā)觀察器刮萌。
注意點
IntersectionObserver API
是異步的驮配,不隨著目標(biāo)元素的滾動同步觸發(fā)。IntersectionObserver
的實現(xiàn)着茸,應(yīng)該采用requestIdleCallback()
壮锻,即只有線程空閑下來,才會執(zhí)行觀察器涮阔。這意味著猜绣,這個觀察器的優(yōu)先級非常低,只在其他任務(wù)執(zhí)行完敬特,瀏覽器有了空閑才會執(zhí)行掰邢。
關(guān)于 requestIdleCallback
可參考: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
兼容性
https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver
polyfill
https://www.npmjs.com/package/intersection-observer
實踐
實例一:圖片懶加載
var url = ''; // 圖片鏈接
var io = new IntersectionObserver(
entries => {
console.log(entries);
entries.forEach(item => {
if(item.isIntersecting) {
// 正常的圖片懶加載設(shè)計應(yīng)該是默認(rèn)加載一張縮略圖,然后等滾動到視口內(nèi)再替換 src 伟阔,這里直接用 innerHTML 方便辣之,只是演示使用
item.target.innerHTML = `<img height="100%" src="${url}" />`;
io.unobserve(item.target)
}
})
}, {
threshold: 1,
root: document.querySelector('.container'),
});
const boxList = document.querySelectorAll('.observer-item');
boxList.forEach(item => {
io.observe(item);
})
實例二:無限滾動
var io = new IntersectionObserver(
entries => {
console.log(entries);
entries.forEach(item => {
if(item.isIntersecting) {
loadItems();
}
})
}, {
threshold: 0.3,
root: document.querySelector('.container'),
});
io.observe(document.querySelector('.observer-target'));
function loadItems(){ /*加載新的items*/}
無限滾動時,最好在頁面底部有一個頁尾欄(又稱sentinels
)皱炉。一旦頁尾欄可見怀估,就表示用戶到達了頁面底部,從而加載新的條目放在頁尾欄前面合搅。這樣做的好處是多搀,不需要再一次調(diào)用observe()
方法,現(xiàn)有的IntersectionObserver
可以保持使用灾部。
實例三:埋點曝光
const boxList = document.querySelectorAll('.mt-table-scroll .mt-table-row');
if(boxList.length <= 0) return;
var io = new IntersectionObserver((entries) =>{
entries.forEach(item => {
// 康铭。。赌髓。 埋點曝光代碼
if (item.isIntersecting) {
// do something
}
io.unobserve(item.target)
}
})
}, {
root: null,
threshold: 0.05, // 閥值設(shè)為0.05从藤,當(dāng)只有比例達到0.05時才觸發(fā)回調(diào)函數(shù)
})
// observe遍歷監(jiān)聽所有box節(jié)點
boxList.forEach(box => io.observe(box))