JavaScript判斷dom元素是否在可視區(qū)域的常規(guī)方式總結(jié)
1. Intersection Observer API(推薦)
這是現(xiàn)代瀏覽器推薦的方法,性能最好,異步執(zhí)行,不會(huì)阻塞主線程。
基礎(chǔ)用法
// 創(chuàng)建觀察器
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('元素進(jìn)入可視區(qū)域', entry.target);
// 可以在這里執(zhí)行懶加載等操作
entry.target.classList.add('visible');
} else {
console.log('元素離開(kāi)可視區(qū)域', entry.target);
entry.target.classList.remove('visible');
}
});
});
// 觀察元素
const elements = document.querySelectorAll('.watch-element');
elements.forEach(el => observer.observe(el));
高級(jí)配置
const options = {
// root: 指定根元素,默認(rèn)為瀏覽器視窗
root: null, // 或者指定特定元素,如 document.querySelector('.container')
// rootMargin: 根的外邊距,可以擴(kuò)大或縮小根的邊界框
rootMargin: '10px 0px -100px 0px', // 上右下左,類似CSS margin
// threshold: 觸發(fā)回調(diào)的可見(jiàn)比例
threshold: [0, 0.25, 0.5, 0.75, 1] // 在0%, 25%, 50%, 75%, 100%可見(jiàn)時(shí)觸發(fā)
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const visiblePercentage = Math.round(entry.intersectionRatio * 100);
console.log(`元素可見(jiàn) ${visiblePercentage}%`);
// 根據(jù)可見(jiàn)比例執(zhí)行不同操作
if (entry.intersectionRatio > 0.5) {
// 超過(guò)50%可見(jiàn)
entry.target.classList.add('mostly-visible');
}
});
}, options);
實(shí)用工具函數(shù)
// 封裝的工具函數(shù)
function createVisibilityObserver(options = {}) {
const defaultOptions = {
root: null,
rootMargin: '0px',
threshold: 0.1
};
const finalOptions = { ...defaultOptions, ...options };
return new IntersectionObserver((entries) => {
entries.forEach(entry => {
const element = entry.target;
const isVisible = entry.isIntersecting;
// 觸發(fā)自定義事件
element.dispatchEvent(new CustomEvent('visibilityChange', {
detail: { isVisible, entry }
}));
});
}, finalOptions);
}
// 使用示例
const observer = createVisibilityObserver({ threshold: 0.5 });
document.querySelectorAll('.lazy-load').forEach(element => {
observer.observe(element);
element.addEventListener('visibilityChange', (e) => {
if (e.detail.isVisible) {
// 執(zhí)行懶加載
const img = element.querySelector('img[data-src]');
if (img) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
}
});
});
2. getBoundingClientRect() 方法
傳統(tǒng)方法,同步執(zhí)行,需要手動(dòng)調(diào)用。
基礎(chǔ)用法
function isInViewport(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= windowHeight &&
rect.right <= windowWidth
);
}
// 使用示例
const element = document.querySelector('.target');
if (isInViewport(element)) {
console.log('元素完全在視窗內(nèi)');
}
部分可見(jiàn)判斷
function isPartiallyInViewport(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
return (
rect.bottom > 0 &&
rect.top < windowHeight &&
rect.right > 0 &&
rect.left < windowWidth
);
}
// 更詳細(xì)的可見(jiàn)性信息
function getVisibilityInfo(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight;
const windowWidth = window.innerWidth;
// 計(jì)算可見(jiàn)區(qū)域
const visibleTop = Math.max(0, rect.top);
const visibleLeft = Math.max(0, rect.left);
const visibleBottom = Math.min(windowHeight, rect.bottom);
const visibleRight = Math.min(windowWidth, rect.right);
const visibleWidth = Math.max(0, visibleRight - visibleLeft);
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
const visibleArea = visibleWidth * visibleHeight;
const totalArea = rect.width * rect.height;
return {
isVisible: visibleArea > 0,
isFullyVisible: isInViewport(element),
visibilityRatio: totalArea > 0 ? visibleArea / totalArea : 0,
rect: rect,
visibleArea: { width: visibleWidth, height: visibleHeight }
};
}
// 使用示例
const element = document.querySelector('.target');
const info = getVisibilityInfo(element);
console.log(`可見(jiàn)比例: ${(info.visibilityRatio * 100).toFixed(2)}%`);
滾動(dòng)監(jiān)聽(tīng)版本
class ScrollVisibilityTracker {
constructor(options = {}) {
this.elements = new Map();
this.threshold = options.threshold || 0.1;
this.throttleDelay = options.throttleDelay || 100;
this.checkVisibility = this.throttle(this.checkVisibility.bind(this), this.throttleDelay);
this.bindEvents();
}
observe(element, callback) {
this.elements.set(element, {
callback,
wasVisible: false
});
// 初始檢查
this.checkElement(element);
}
unobserve(element) {
this.elements.delete(element);
}
checkVisibility() {
this.elements.forEach((data, element) => {
this.checkElement(element);
});
}
checkElement(element) {
const data = this.elements.get(element);
if (!data) return;
const info = getVisibilityInfo(element);
const isVisible = info.visibilityRatio >= this.threshold;
if (isVisible !== data.wasVisible) {
data.wasVisible = isVisible;
data.callback(isVisible, info);
}
}
bindEvents() {
window.addEventListener('scroll', this.checkVisibility, { passive: true });
window.addEventListener('resize', this.checkVisibility);
}
destroy() {
window.removeEventListener('scroll', this.checkVisibility);
window.removeEventListener('resize', this.checkVisibility);
this.elements.clear();
}
throttle(func, delay) {
let timeoutId;
let lastExecTime = 0;
return function (...args) {
const currentTime = Date.now();
if (currentTime - lastExecTime > delay) {
func.apply(this, args);
lastExecTime = currentTime;
} else {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
lastExecTime = Date.now();
}, delay - (currentTime - lastExecTime));
}
};
}
}
// 使用示例
const tracker = new ScrollVisibilityTracker({ threshold: 0.5 });
document.querySelectorAll('.track-element').forEach(element => {
tracker.observe(element, (isVisible, info) => {
if (isVisible) {
element.classList.add('in-view');
console.log('元素進(jìn)入視窗', info);
} else {
element.classList.remove('in-view');
}
});
});
3. 特殊場(chǎng)景的解決方案
在滾動(dòng)容器中的元素
function isInScrollContainer(element, container) {
const elementRect = element.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
return (
elementRect.top >= containerRect.top &&
elementRect.left >= containerRect.left &&
elementRect.bottom <= containerRect.bottom &&
elementRect.right <= containerRect.right
);
}
// 使用Intersection Observer觀察滾動(dòng)容器
function createContainerObserver(container) {
return new IntersectionObserver((entries) => {
entries.forEach(entry => {
console.log('容器內(nèi)元素可見(jiàn)性變化', entry.isIntersecting);
});
}, {
root: container, // 指定容器為根元素
threshold: 0.1
});
}
考慮CSS Transform的情況
function getTransformedBounds(element) {
const rect = element.getBoundingClientRect();
// 如果元素有CSS transform,getBoundingClientRect已經(jīng)包含了變換后的位置
// 不需要額外計(jì)算
return rect;
}
// 對(duì)于復(fù)雜的3D變換,可能需要更精確的計(jì)算
function isTransformedElementVisible(element) {
const rect = element.getBoundingClientRect();
// 檢查元素是否因?yàn)閠ransform: scale(0)等而不可見(jiàn)
const computedStyle = getComputedStyle(element);
const transform = computedStyle.transform;
if (transform === 'none') {
return isPartiallyInViewport(element);
}
// 檢查是否有scale(0)或類似的變換
if (rect.width === 0 || rect.height === 0) {
return false;
}
return isPartiallyInViewport(element);
}
4. 性能優(yōu)化技巧
虛擬滾動(dòng)場(chǎng)景
class VirtualScrollObserver {
constructor(container, options = {}) {
this.container = container;
this.itemHeight = options.itemHeight || 100;
this.buffer = options.buffer || 5; // 緩沖區(qū)項(xiàng)目數(shù)量
this.items = [];
this.visibleRange = { start: 0, end: 0 };
this.handleScroll = this.throttle(this.calculateVisibleRange.bind(this), 16);
this.container.addEventListener('scroll', this.handleScroll);
}
calculateVisibleRange() {
const scrollTop = this.container.scrollTop;
const containerHeight = this.container.clientHeight;
const start = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer);
const end = Math.min(
this.items.length - 1,
Math.ceil((scrollTop + containerHeight) / this.itemHeight) + this.buffer
);
if (start !== this.visibleRange.start || end !== this.visibleRange.end) {
this.visibleRange = { start, end };
this.onVisibleRangeChange(this.visibleRange);
}
}
onVisibleRangeChange(range) {
// 子類實(shí)現(xiàn)或通過(guò)回調(diào)處理
console.log('可見(jiàn)范圍變化:', range);
}
throttle(func, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
func.apply(this, args);
lastTime = now;
}
};
}
}
懶加載圖片完整實(shí)現(xiàn)
class LazyImageLoader {
constructor(options = {}) {
this.options = {
rootMargin: '50px',
threshold: 0.1,
loadingClass: 'lazy-loading',
loadedClass: 'lazy-loaded',
errorClass: 'lazy-error',
...options
};
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{
rootMargin: this.options.rootMargin,
threshold: this.options.threshold
}
);
this.loadingImages = new Set();
}
observe(img) {
if (!(img instanceof HTMLImageElement)) {
console.warn('LazyImageLoader: 只能觀察img元素');
return;
}
if (!img.dataset.src && !img.dataset.srcset) {
console.warn('LazyImageLoader: 圖片缺少data-src或data-srcset屬性');
return;
}
this.observer.observe(img);
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.observer.unobserve(entry.target);
}
});
}
loadImage(img) {
if (this.loadingImages.has(img)) return;
this.loadingImages.add(img);
img.classList.add(this.options.loadingClass);
const tempImg = new Image();
tempImg.onload = () => {
this.applyImage(img, tempImg);
img.classList.remove(this.options.loadingClass);
img.classList.add(this.options.loadedClass);
this.loadingImages.delete(img);
};
tempImg.onerror = () => {
img.classList.remove(this.options.loadingClass);
img.classList.add(this.options.errorClass);
this.loadingImages.delete(img);
};
// 支持srcset
if (img.dataset.srcset) {
tempImg.srcset = img.dataset.srcset;
}
tempImg.src = img.dataset.src;
}
applyImage(img, tempImg) {
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
delete img.dataset.srcset;
}
img.src = tempImg.src;
delete img.dataset.src;
}
destroy() {
this.observer.disconnect();
this.loadingImages.clear();
}
}
// 使用示例
const lazyLoader = new LazyImageLoader({
rootMargin: '100px',
threshold: 0.1
});
document.querySelectorAll('img[data-src]').forEach(img => {
lazyLoader.observe(img);
});
5. 兼容性處理
// Intersection Observer polyfill檢查
function createCompatibleObserver(callback, options) {
if ('IntersectionObserver' in window) {
return new IntersectionObserver(callback, options);
} else {
console.warn('IntersectionObserver not supported, falling back to scroll listener');
return createScrollBasedObserver(callback, options);
}
}
function createScrollBasedObserver(callback, options = {}) {
const elements = new Set();
const threshold = options.threshold || 0;
function checkElements() {
const entries = [];
elements.forEach(element => {
const info = getVisibilityInfo(element);
const isIntersecting = info.visibilityRatio >= threshold;
entries.push({
target: element,
isIntersecting,
intersectionRatio: info.visibilityRatio,
boundingClientRect: info.rect
});
});
if (entries.length > 0) {
callback(entries);
}
}
const throttledCheck = throttle(checkElements, 100);
window.addEventListener('scroll', throttledCheck, { passive: true });
window.addEventListener('resize', throttledCheck);
return {
observe(element) {
elements.add(element);
// 立即檢查一次
setTimeout(() => {
const info = getVisibilityInfo(element);
const isIntersecting = info.visibilityRatio >= threshold;
callback([{
target: element,
isIntersecting,
intersectionRatio: info.visibilityRatio,
boundingClientRect: info.rect
}]);
}, 0);
},
unobserve(element) {
elements.delete(element);
},
disconnect() {
window.removeEventListener('scroll', throttledCheck);
window.removeEventListener('resize', throttledCheck);
elements.clear();
}
};
}
總結(jié)
選擇合適的方法:
- Intersection Observer API - 現(xiàn)代瀏覽器首選,性能最佳
- getBoundingClientRect + 滾動(dòng)監(jiān)聽(tīng) - 需要兼容老瀏覽器時(shí)使用
- 虛擬滾動(dòng) - 處理大量元素時(shí)的特殊優(yōu)化
關(guān)鍵考慮因素:
- 性能:Intersection Observer > 節(jié)流的滾動(dòng)監(jiān)聽(tīng) > 頻繁的滾動(dòng)監(jiān)聽(tīng)
- 精確度:getBoundingClientRect更精確,但需要手動(dòng)觸發(fā)
- 兼容性:getBoundingClientRect支持更老的瀏覽器
- 功能需求:是否需要部分可見(jiàn)、可見(jiàn)比例等詳細(xì)信息
到此這篇關(guān)于JavaScript判斷dom元素是否在可視區(qū)域的常規(guī)方式總結(jié)的文章就介紹到這了,更多相關(guān)JavaScript判斷dom元素內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript實(shí)現(xiàn)懸浮跟隨框緩動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了javascript實(shí)現(xiàn)懸浮跟隨框緩動(dòng)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
使用typescript快速開(kāi)發(fā)一個(gè)cli的實(shí)現(xiàn)示例
這篇文章主要介紹了使用typescript快速開(kāi)發(fā)一個(gè)cli的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
javascript實(shí)現(xiàn)異形滾動(dòng)輪播
這篇文章主要為大家詳細(xì)介紹了javascript實(shí)現(xiàn)異形滾動(dòng)輪播,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11
js實(shí)現(xiàn)九宮格圖片半透明漸顯特效的方法
這篇文章主要介紹了js實(shí)現(xiàn)九宮格圖片半透明漸顯特效的方法,涉及js操作css特效的技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-02-02
javascript 網(wǎng)頁(yè)跳轉(zhuǎn)的方法
昨天練習(xí)的時(shí)候正好要用到跳轉(zhuǎn)代碼,在網(wǎng)上找了一下,覺(jué)得下面幾個(gè)不錯(cuò)...整理了一下發(fā)上來(lái)...2008-12-12
基于Bootstrap框架實(shí)現(xiàn)圖片切換
這篇文章主要介紹了基于Bootstrap框架實(shí)現(xiàn)圖片切換的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-03-03

