demo位址
懶加載
Lazyload 可以加快網頁通路速度,減少請求,實作思路就是判斷圖檔元素是否可見來決定是否加載圖檔。當圖檔位于浏覽器視口 (viewport) 中時,動态設定
<img>
标簽的 src 屬性,浏覽器會根據 src 屬性發送請求加載圖檔。
懶加載實作
首先不設定 src 屬性,将圖檔真正的 url 放在另外一個屬性 data-src 中,在圖檔即将進入浏覽器可視區域之前,将 url 取出放到 src 中。
懶加載的關鍵是如何判斷圖檔處于浏覽器可視範圍内,通常有三種方法:
方法一
通過對比螢幕可視視窗高度和浏覽器滾動距離與元素相對文檔頂部的距離之間的關系,判斷元素是否可見。
示意圖如下:
代碼如下:
function isInSight(el) {
const clientHeight = window.innerHeight // 擷取螢幕可視視窗高度
const scrollTop = document.body.scrollTop // 浏覽器視窗頂部與文檔頂部之間的距離
// el.offsetTop 元素相對于文檔頂部的距離
// +100是為了提前加載
return el.offsetTop <= clientHeight + scrollTop + 100
}
複制
方法二
通過 getBoundingClientRect() 擷取圖檔相對于浏覽器視窗的位置
示意圖如下:
getBoundingClientRect() 方法傳回一個 ClientRect 對象,裡面包含元素的位置和大小的資訊
ClientRect {
bottom: 596,
height: 596,
left: 0,
right: 1920,
top: 0,
width: 1920
}
複制
其中位置是相對于浏覽器視圖左上角而言。代碼如下:
function isInSight1(el) {
const bound = el.getBoundingClientRect()
const clientHeight = window.innerHeight // 表示浏覽器可視區域的高度
// bound.top 表示圖檔到可視區域頂部距離
// +100是為了提前加載
return bound.top <= clientHeight + 100
}
複制
方法三
使用 IntersectionObserver API,觀察元素是否可見。“可見”的本質是目标元素與 viewport 是否有交叉區,是以這個 API 叫做“交叉觀察器”。
實作方式
function loadImg(el) {
if (!el.src) {
const source = el.dataset.src
el.src = source
el.removeAttribute('data-src')
}
}
const io = new IntersectionObserver(entries => {
for (const entry of entries) {
const el = entry.target
const intersectionRatio = entry.intersectionRatio
if (intersectionRatio > 0 && intersectionRatio <= 1) {
loadImg(el)
}
el.onload = el.onerror = () => io.unobserve(el)
}
})
function checkImgs() {
const imgs = Array.from(document.querySelectorAll('img[data-src]'))
imgs.forEach(item => io.observe(item))
}
複制
IntersectionObserver
IntersectionObserver 的作用就是檢測一個元素是否可見,以及元素什麼時候進入或者離開浏覽器視口。
相容性
- Chrome 51+(釋出于 2016-05-25)
- Android 5+ (Chrome 56 釋出于 2017-02-06)
- Edge 15 (2017-04-11)
- iOS 不支援
Polyfill
WICG 提供了一個 polyfill
API
const io = new IntersectionObserver(callback, option)
複制
IntersectionObserver 是一個構造函數,接受兩個參數,第一個參數是可見性變化時的回調函數,第二個參數定制了一些關于可見性的參數(可選),IntersectionObserver 執行個體化後傳回一個觀察器,可以指定觀察哪些 DOM 節點。
下面是一個最簡單的應用:
// 1. 擷取 img
const img = document.querySelector('img')
// 2. 執行個體化 IntersectionObserver,添加 img 出現在 viewport 瞬間的回調
const observer = new IntersectionObserver(changes => {
console.log('我出現了!')
});
// 3. 開始監聽 img
observer.observe(img)
複制
(1) callback
回調 callback 接受一個數組作為參數,數組元素是 IntersectionObserverEntry 對象。IntersectionObserverEntry 對象上有7個屬性,
IntersectionObserverEntry {
time: 72.15500000000002,
rootBounds: ClientRect,
boundingClientRect: ClientRect,
intersectionRatio: 0.4502074718475342,
intersectionRect: ClientRect,
isIntersecting: true,
target: img
}
複制
- boundingClientRect: 對 observe 的元素執行 getBoundingClientRect 的結果
- rootBounds: 對根視圖執行 getBoundingClientRect 的結果
- intersectionRect: 目标元素與視口(或根元素)的交叉區域的資訊
- target: observe 的對象,如上述代碼就是 img
- time: 過了多久才出現在 viewport 内
- intersectionRatio:目标元素的可見比例,intersectionRect 占 boundingClientRect 的比例,完全可見時為1,完全不可見時小于等于0
- isIntersecting: 目标元素是否處于視口中
(2) option
假如我們需要特殊的觸發條件,比如元素可見性為一半的時候觸發,或者我們需要更改根元素,這時就需要配置第二個參數 option 了。
通過設定 option 的 threshold 改變回調函數的觸發條件,threshold 是一個範圍為0到1數組,預設值是[0],也就是在元素可見高度變為0時就會觸發。如果指派為 [0, 0.5, 1],那回調就會在元素可見高度是0%,50%,100%時,各觸發一次回調。
const observer = new IntersectionObserver((changes) => {
console.log(changes.length);
}, {
root: null,
rootMargin: '20px',
threshold: [0, 0.5, 1]
});
複制
root 參數預設是 null,也就是浏覽器的 viewport,可以設定為其它元素,rootMargin 參數可以給 root 元素添加一個 margin,如
rootMargin: '20px'
時,回調會在元素出現前 20px 提前調用,消失後延遲 20px 調用回調。
(3) 觀察器
// 開始觀察
io.observe(document.getElementById('root'))
// 觀察多個 DOM 元素
io.observe(elementA)
io.observe(elementB)
// 停止觀察
io.unobserve(element)
// 關閉觀察器
io.disconnect()
複制
使用 IntersectionObserver 優勢
使用前兩種方式實作 lazyload 都需要監聽浏覽器 scroll 事件,而且要對每個目标元素執行 getBoundingClientRect() 方法以擷取所需資訊,這些代碼都在主線程上運作,是以可能造成性能問題。
Intersection Observer API 會注冊一個回調方法,每當期望被監視的元素進入或者退出另外一個元素的時候(或者浏覽器的視口)該回調方法将會被執行,或者兩個元素的交集部分大小發生變化的時候回調方法也會被執行。通過這種方式,網站将不需要為了監聽兩個元素的交集變化而在主線程裡面做任何操作,并且浏覽器可以幫助我們優化和管理兩個元素的交集變化。
參考資料
- 原生 JS 實作最簡單的圖檔懶加載
- IntersectionObserver
- IntersectionObserver API 使用教程
- MDN-Intersection Observer API