一次DOM曝光封裝歷程
初版
邏輯:window.scroll 監(jiān)聽滾動(dòng) + 使用 getBoundingClientRect() 相對(duì)于視口位置實(shí)現(xiàn)
具體代碼如下:
function buryExposure (el, fn) {
/*
* 省略一些邊界判斷
* ......
**/
let elEnter = false; // dom是否進(jìn)入可視區(qū)域
el.exposure = () => {
const { top } = el.getBoundingClientRect();
if (top > 0 && top < window.screen.availHeight) {
if (!elEnter) {
elEnter = true;
fn(el)
}
} else {
elEnter = false;
};
}
document.addEventListener('scroll', el.exposure);
}
回調(diào)傳出 el ,一般為頁面注銷時(shí)注銷對(duì)應(yīng)滾動(dòng)事件: el.exposure
其中兩個(gè)點(diǎn)
第一個(gè):
// 判斷上邊距出現(xiàn)在視口內(nèi),則判定為曝光
const { top } = el.getBoundingClientRect();
if (top > 0 && top < window.screen.availHeight)
其中這里的 top 以及其他邊距對(duì)應(yīng)視口計(jì)算方式可能和你想象的不一樣,上圖摘自 你真的會(huì)用getBoundingClientRect 嗎 (https://juejin.im/entry/59c1fd23f265da06594316a9), 它對(duì)這個(gè)屬性講的比較詳細(xì)可以看看
第二個(gè):
let elEnter = false; //
用一個(gè)變量來控制當(dāng) dom 已經(jīng)曝光則不再持續(xù),直到 dom 離開視口,重新計(jì)算
重寫
當(dāng)我以為已經(jīng)夠用時(shí),某次需求需要監(jiān)聽 DOM 在某個(gè) div 內(nèi)橫向滑動(dòng)的曝光,發(fā)現(xiàn)它并不支持!而后面一些曝光策略對(duì)比的文章說到這個(gè) getBoundingClientRect API 會(huì)引起性能問題
不相信的你可以試一下?。?!
于是我就開啟 google 大法和在掘金社區(qū)內(nèi)搜一些曝光的文章,然后我就發(fā)現(xiàn)了新大陸!
window.IntersectionObserver
這次曝光的主角:優(yōu)先使用異步觀察目標(biāo)元素與祖先元素或頂級(jí)文檔viewport的交集中的變化的方法
關(guān)于他的具體介紹,我這里簡(jiǎn)單講一下我用到的屬性,具體可查閱 超好用的 API 之 IntersectionObserver (https://juejin.im/post/5d11ced1f265da1b7004b6f7) 或者 MDN (https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver)
主要使用如下:
const io = new IntersectionObserver(callback, options)
io.observe(DOM)
callback 回調(diào)函數(shù),options 是配置參數(shù)
io.observe 觀察函數(shù),DOM 為被觀察對(duì)象
主要兩點(diǎn)
1.options 的配置為:
const observerOptions = {
root: null, // 默認(rèn)使用視口作為交集對(duì)象
rootMargin: '0px', // 無樣式
threshold: [...Array(100).keys()].map(x => x / 100) // 監(jiān)聽交集時(shí)的每0.01變化觸發(fā)callback回調(diào)
}
2.callback 函數(shù)如下:
(entries) => {
// 過程性監(jiān)聽
entries.forEach((item) => {
if (item.intersectionRatio > 0 && item.intersectionRatio <= 1) { // 部分顯示即為顯示
// todo....
} else if (item.intersectionRatio === 0) { // 不可見時(shí)恢復(fù)
// todo...
}
});
}
曝光的判斷來自以下第二種(部分顯示則曝光):
intersectionRatio === 1:則監(jiān)聽對(duì)象完整顯示
intersectionRatio > 0 && intersectionRatio < 1 : 則監(jiān)聽對(duì)象部分顯示
intersectionRatio === 0:則監(jiān)聽對(duì)象不顯示其實(shí) entries[] 子元素對(duì)象還有一個(gè)屬性:isIntersecting
返回一個(gè)布爾值,下列兩種操作均會(huì)觸發(fā) callback:
如果目標(biāo)元素出現(xiàn)在 root 可視區(qū),返回 true。
如果從 root 可視區(qū)消失,返回 false
按理說應(yīng)該是使用它,但是發(fā)現(xiàn)不適合現(xiàn)實(shí)場(chǎng)景?。?!
比如 類 banner 橫向移動(dòng) (https://jsbin.com/vuzujiw/6/edit?html,css,js,console,output),我第一調(diào)試的時(shí)候就碰到了
用戶要看的子元素是被父元素給限制住了,但是對(duì)于 isIntersecting 它來講是出現(xiàn)在視口內(nèi)的。
最終版
考慮兼容性:
// 使用w3c出的polyfill
require('intersection-observer');
主要邏輯如下:
/**
* DOM曝光
* @param {object} options 配置參數(shù)
* options @param {Array} DOMs 要被監(jiān)聽的DOM列表
* options @param {Function} callback[type, io] 回調(diào),傳入?yún)?shù)
* options @param {DOM} parentDom 子元素的對(duì)應(yīng)父元素
*/
export default function expose (options = {}) {
if (!options.DOMs || !options.callback) {
console.error('Error: 傳入監(jiān)聽DOM或者回調(diào)函數(shù)');
return;
}
const observerOptions = {
root: null,
rootMargin: '0px',
threshold: [...Array(100).keys()].map(x => x / 100)
};
options.parentDom && (observerOptions.root = options.parentDom);
// 優(yōu)先使用異步觀察目標(biāo)元素與祖先元素或頂級(jí)文檔viewport的交集中的變化的方法
if (window.IntersectionObserver) {
let elEnter = false; // dom是否進(jìn)入可視區(qū)域
const io = new IntersectionObserver((entries) => {
// 回調(diào)包裝
const fn = () => options.callback({ io });
// 過程性監(jiān)聽
entries.forEach((item) => {
if (!elEnter && item.intersectionRatio > 0 && item.intersectionRatio <= 1) { // 部分顯示即為顯示
fn();
elEnter = true;
} else if (item.intersectionRatio === 0) { // 不可見時(shí)恢復(fù)
elEnter = false;
}
});
}, observerOptions);
// 監(jiān)聽DOM
options.DOMs.forEach(DOM => io.observe(DOM));
}
}
參考文獻(xiàn)
你真的會(huì)用 getBoundingClientRect 嗎(https://juejin.im/entry/59c1fd23f265da06594316a9)
作者:
歡迎關(guān)注微信公眾號(hào) :政采云前端團(tuán)隊(duì)