首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

jQuery 瀑布流滾動加載實現:IntersectionObserver 現代方案與傳統 scroll 事件對比

     編輯:PHP 相關知識

jQuery 瀑布流滾動加載實現:IntersectionObserver 現代方案與傳統 scroll 事件對比

瀑布流無限滾動的核心是「檢測用戶滾動到底部」。傳統 scroll 事件頻繁觸發、性能差;現代 IntersectionObserver API 異步觀察、零性能損耗。本文對比兩種方案並提供生產級代碼。

一、傳統 Scroll 事件方案(兼容性好)

<script>
// 防抖節流工具
function debounce(fn, delay) {
    let timer = null;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
    };
}

function throttle(fn, limit) {
    let inThrottle = false;
    return (...args) => {
        if (!inThrottle) {
            fn.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

class ScrollLoader {
    constructor(options = {}) {
        this.container = options.container || window;
        this.triggerEl = options.triggerEl; // 底部哨兵元素
        this.callback = options.callback;
        this.threshold = options.threshold || 300; // px
        this.debounceMs = options.debounceMs || 100;
        
        this.boundCheck = debounce(this.check.bind(this), this.debounceMs);
        this.bind();
    }
    
    bind() {
        if (this.triggerEl) {
            // 哨兵模式:監聽哨兵元素是否進入可視區
            this.observer = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting && !this.loading) {
                        this.loadMore();
                    }
                });
            }, {rootMargin: '200px'}); // 提前 200px 觸發
            this.observer.observe(this.triggerEl);
        } else {
            // 傳統 scroll 監聽
            $(this.container).on('scroll', this.boundCheck);
        }
    }
    
    check() {
        const scrollTop = this.container === window ? $(window).scrollTop() : $(this.container).scrollTop();
        const windowHeight = this.container === window ? $(window).height() : $(this.container).height();
        const docHeight = this.container === window ? $(document).height() : $(this.container)[0].scrollHeight;
        
        if (scrollTop + windowHeight >= docHeight - this.threshold) {
            this.loadMore();
        }
    }
    
    async loadMore() {
        if (this.loading) return;
        this.loading = true;
        try {
            await this.callback();
        } finally {
            this.loading = false;
        }
    }
    
    destroy() {
        if (this.observer) this.observer.disconnect();
        $(this.container).off('scroll', this.boundCheck);
    }
}

// 使用
const loader = new ScrollLoader({
    triggerEl: document.getElementById('sentinel'), // 推薦:哨兵元素
    callback: async () => {
        const data = await fetchNextPage();
        renderItems(data);
    }
});
</script>

二、IntersectionObserver 現代方案(推薦)

<script>
class IntersectionLoader {
    constructor(options = {}) {
        this.sentinel = options.sentinel; // 必須:底部哨兵元素
        this.callback = options.callback; // 加載回調
        this.rootMargin = options.rootMargin || '200px 0px'; // 提前觸發距離
        this.threshold = options.threshold || 0;
        this.loading = false;
        this.observer = null;
        
        if (!this.sentinel) {
            throw new Error('IntersectionLoader 需要 sentinel 元素');
        }
        this.init();
    }
    
    init() {
        this.observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting && !this.loading) {
                    this.loadMore();
                }
            });
        }, {
            root: null, // viewport
            rootMargin: this.rootMargin,
            threshold: this.threshold,
        });
        
        this.observer.observe(this.sentinel);
    }
    
    async loadMore() {
        if (this.loading) return;
        this.loading = true;
        this.showLoading(true);
        
        try {
            const hasMore = await this.callback();
            if (!hasMore) this.disconnect();
        } catch (e) {
            console.error('加載失敗:', e);
            this.showError(e.message);
        } finally {
            this.loading = false;
            this.showLoading(false);
        }
    }
    
    showLoading(show) {
        const loader = document.getElementById('loading-indicator');
        if (loader) loader.style.display = show ? 'block' : 'none';
    }
    
    showError(msg) {
        // 顯示錯誤提示,提供重試按鈕
    }
    
    disconnect() {
        if (this.observer) {
            this.observer.disconnect();
            this.observer = null;
        }
    }
    
    // 手動觸發(如用戶點擊「加載更多」)
    trigger() {
        this.loadMore();
    }
}

// HTML 結構
/*
*/ // 使用 const loader = new IntersectionLoader({ sentinel: document.getElementById('sentinel'), rootMargin: '300px 0px', // 提前 300px 預加載 callback: async () => { const page = window.__page || 1; const res = await fetch(`/api/articles?page=${page + 1}`); const data = await res.json(); if (data.items?.length) { renderItems(data.items); window.__page = page + 1; return data.pagination?.hasMore ?? true; } return false; // 無更多數據 } }); </script>

三、瀑布流佈局與滾動加載完整整合

<script>
class WaterfallInfiniteScroll {
    constructor(options = {}) {
        this.container = options.container; // 容器元素
        this.itemSelector = options.itemSelector || '.waterfall-item';
        this.columnWidth = options.columnWidth || 360;
        this.gap = options.gap || 15;
        this.apiUrl = options.apiUrl;
        this.page = 1;
        this.hasMore = true;
        this.loading = false;
        
        this.sentinel = document.createElement('div');
        this.sentinel.id = 'waterfall-sentinel';
        this.sentinel.style.cssText = 'height:1px;';
        this.container.appendChild(this.sentinel);
        
        this.loader = new IntersectionLoader({
            sentinel: this.sentinel,
            rootMargin: '300px 0px',
            callback: () => this.loadPage(),
        });
        
        this.layout();
        window.addEventListener('resize', this.debounce(() => this.layout(), 200));
    }
    
    async loadPage() {
        if (this.loading || !this.hasMore) return false;
        this.loading = true;
        
        try {
            const res = await fetch(`${this.apiUrl}?page=${this.page + 1}`);
            const data = await res.json();
            
            if (data.items?.length) {
                this.appendItems(data.items);
                this.page++;
                this.hasMore = data.pagination?.hasMore ?? true;
                return this.hasMore;
            }
            this.hasMore = false;
            return false;
        } catch (e) {
            console.error('加載失敗:', e);
            throw e;
        } finally {
            this.loading = false;
        }
    }
    
    appendItems(items) {
        const fragment = document.createDocumentFragment();
        items.forEach(item => {
            const div = document.createElement('div');
            div.className = 'waterfall-item';
            div.innerHTML = this.renderCard(item);
            fragment.appendChild(div);
        });
        this.container.insertBefore(fragment, this.sentinel);
        this.layout();
    }
    
    renderCard(item) {
        return `
            
                ${item.title}
                
${item.title}

${item.content}

`; } layout() { const containerWidth = this.container.clientWidth; const cols = Math.max(1, Math.floor((containerWidth + this.gap) / (this.columnWidth + this.gap))); const colHeights = new Array(cols).fill(0); const colLefts = Array.from({length: cols}, (_, i) => i * (this.columnWidth + this.gap)); const items = this.container.querySelectorAll(this.itemSelector); items.forEach(item => { if (item === this.sentinel) return; const height = item.offsetHeight; const minCol = colHeights.indexOf(Math.min(...colHeights)); item.style.position = 'absolute'; item.style.left = colLefts[minCol] + 'px'; item.style.top = colHeights[minCol] + 'px'; item.style.width = this.columnWidth + 'px'; colHeights[minCol] += height + this.gap; }); this.container.style.height = Math.max(...colHeights) + 'px'; } debounce(fn, delay) { let timer = null; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } } // 初始化 document.addEventListener('DOMContentLoaded', () => { new WaterfallInfiniteScroll({ container: document.getElementById('waterfall'), apiUrl: '/api/articles', columnWidth: 360, gap: 15, }); }); </script>

四、方案對比與選擇建議

維度Scroll + 防抖IntersectionObserver
性能高頻觸發,主線程繁忙異步觀察,零主線程開銷
準確性需手動計算閾值瀏覽器原生精準檢測
預加載靠 threshold 控制rootMargin 靈活配置
兼容性IE6+ 全支援IE 不支援,需 Polyfill
代碼複雜度低中(需哨兵元素)
移動端體驗滾動卡頓明顯絲滑流暢

五、Polyfill 方案(支援 IE)

<!-- 在 head 中引入 -->
<script src="https://cdn.jsdelivr.net/npm/intersection-observer@0.12.2/intersection-observer.min.js"></script>

<script>
// 檢測並加載 Polyfill
if (!('IntersectionObserver' in window)) {
    const script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/intersection-observer@0.12.2/intersection-observer.min.js';
    script.onload = () => initWaterfall();
    document.head.appendChild(script);
} else {
    initWaterfall();
}
</script>

六、進階優化清單

  1. 虛擬滾動:項目 > 500 時移除可視區外 DOM,保持 translate3d 位置
  2. 圖片懶加載:原生 loading="lazy" + IntersectionObserver 降級
  3. 預測性預加載:分析用戶滾動速度,提前 2-3 頁請求
  4. 狀態持久化:sessionStorage 保存頁碼/滾動位置,刷新恢復
  5. 錯誤重試:指數退避重試,最多 3 次,展示「點擊重試」
  6. SEO 友好:SSR 渲染首屏,history.pushState 更新 URL

現代瀏覽器環境下,IntersectionObserver + 哨兵元素 是實現瀑布流無限滾動的最佳方案,性能優越、代碼優雅、體驗流暢。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved