為什麼需要懶載入
通常使用者開啟網頁時,整個網頁的內容將被下載並且呈現在一個頁面中,雖然允許瀏覽器快取頁面,但是不能保證使用者檢視所有下載的的內容,例如一個照片牆應用,可能使用者僅僅檢視第一個圖片之後離開,結果就是白白浪費了記憶體和頻寬。因此我們需要當使用者需要訪問頁面的一部分時才去載入內容,而不是一看是就去載入全部內容。
如何實現懶載入
當有人向網頁(影像,視訊)等資源,資源引用一個小的佔位符,當使用者瀏覽網頁,實際的資源被瀏覽器快取,並且當資源在螢幕上可見時替換佔位符,例如,如果使用者載入網頁並立即離開網頁,則除了網頁的頂部之外沒有任何內容被載入。
懶載入具體實現
以載入圖片為例子,我們需要將img
標籤中設定一個data-src
屬性,它指向的是實際上我們需要載入的影像,而img
的src
指向一張預設的圖片,如果為空的話也會向伺服器傳送請求。
<img src="default.jpg" data-src="www.example.com/1.jpg">
複製程式碼
之後當使用者訪問的可視區域的img
元素時,將src
得值替換為data-src
指向的實際資源載入的影像
具體程式碼
const lazy = (el) => {
let scrTop = getTop();
let windowHeight = document.documentElement.clientHeight;
function getTop(){
return document.documentElement.scrollTop || document.body.scrollTop;
}
function getOffset(node){
return node.getBoundingClientRect().top + scrTop;
}
function inView(node){
// 設立閾值
const threshold = 0;
const viewTop = scrTop;
const viewBot = viewTop + windowHeight;
const nodeTop = getOffset(node);
const nodeBot = nodeTop + node.offsetHeight;
const offset = (threshold / 100) * windowHeight;
console.log((nodeBot >= viewTop - offset), (nodeTop <= viewBot + offset))
return (nodeBot >= viewTop - offset) && (nodeTop <= viewBot + offset)
}
function check(node){
let el = document.querySelector(node);
let images = [...el.querySelectorAll('img')];
images.forEach(img => {
if(inView(img)){
img.src = img.dataset.src;
}
})
}
check(el);
}
window.onscroll = function(){
lazy('.foo');
}
複製程式碼
現代化懶載入實現方法
通過上面例子的實現,我們要實現懶載入都需要去監聽scroll事件,儘管我們可以通過函式節流的方式來阻止高頻率的執行函式,但是我們還是需要去計算scrollTop
,offsetHeight
等屬性,有沒有簡單的不需要計算這些屬性的方式呢,答案是有的---IntersectionObserver
根據MDN:
IntersectionObserver API為開發者提供了一種可以非同步監聽目標元素與其祖先或視窗(viewport)處於交叉狀態的方式。祖先元素與視窗(viewport)被稱為根(root)。
簡單來說就是觀察一個元素和另一個元素是否重疊。
IntersectionObserver初始化的過程中提供了三個主要元素的配置:
- root: 這是用於觀察的根元素。他定義了可觀察元素的基本捕獲框架,預設情況下,
root
指向的是瀏覽器的視口,但實際上可以是任意的DOM元素,要注意的是:root
在這種情況下,要觀察元素的必選要在root代表的Dom元素內部 - rootMargin: 計算交叉時新增到根(root)邊界盒bounding box的矩形偏移量, 可以有效的縮小或擴大根的判定範圍從而滿足計算需要。值得選項與
margin
CSS類似,比如rootMargin: '50px 20px 10px 40px'
(top, right, bottom, left) - threshold: 一個包含閾值的list, 升序排列, list中的每個閾值都是監聽物件的交叉區域與邊界區域的比率。當監聽物件的任何閾值被越過時,都會生成一個通知(Notification)。如果構造器未傳入值, 則預設值為0.
為了告訴我們
intersectionObserver
我們想要得配置,我們只需要將我們得config
物件和我們的回撥函式一起傳遞到Observer建構函式中
const config = {
root: null,
rootMargin: '0px',
threshold: 0.5
}
let observer = new IntersectionObserver(fucntion(entries){
// ...
}, config)
複製程式碼
現在我們需要去給IntersectionObserver
實際觀察的元素
const img = document.querySelector('image');
observer.observe(img);
複製程式碼
關於這個實際觀察的元素需要注意幾點
- 首先他應該位於
root
代表的DOM元素中 IntersectionObserver
一次只能接受一個觀察元素,不支援批量觀察。這意味著如果你需要觀察幾個元素(比如說一個頁面上的幾個影像),你必須遍歷所有元素並分別觀察它們中的每一個
const images = document.querySelecttorAll('img');
images.forEach(image => {
observer.observe(image)
})
複製程式碼
- 當使用Observer載入頁面時,您可能會注意到,IntersectionObserver所有觀察到的元素的回撥已經被觸發了。我們可以通過回撥函式來解決這個問題
IntersectionObserver回撥函式
new IntersectionObserver(function(entries, self))
複製程式碼
在entries
我們得到我們的回撥函式作為Array是特殊型別的:IntersectionObserverEntry
首先IntersectionObserverEntry
含有三個不同的矩形的資訊
- rootBounds: '捕捉框架(
root
+rootMargin
)'的矩形 - boundClientRect: 觀察元素本身的矩形
- intersectionRect: 捕捉框架和觀察元素相交的矩形
此外,
IntersectionObserverEntry
還提供了isIntersecting
,這是一個方便的屬性,返回觀察元素是否與捕獲框架相交, 另外,IntersectionObserverEntry
提供了利於計算的遍歷屬性intersctionRatio
:返回intersectionRect 與 boundingClientRect 的比例值.target
則返回要觀察的元素 好了
簡單介紹完,讓我們回到正題,用這個IntersectionObserver
來實現代化的懶載入方式吧
const images = document.querySelectorAll('[data-src]')
const config = {
rootMargin: '0px',
threshold: 0
};
let observer = new IntersectionObserver((entries, self)=>{
entries.forEach(entry => {
if(entry.isIntersecting){
// 載入影像
preloadImage(entry.target);
// 解除觀察
self.unobserve(entry.target)
}
})
}, config)
images.forEach(image => {
observer.observe(image);
});
function preloadImage(img) {
const src = img.dataset.src
if (!src) { return; }
img.src = src;
}
複製程式碼
相比於之前懶載入的方式是不是更加簡潔,而且只有當觀察元素和捕捉框架交叉或重疊時,才會觸發回掉函式(載入頁面時也會觸發回撥函式,不過我們可以用isIntersecting
來進行判斷是否和觀察元素相交)
延遲載入的好處
- 延遲載入在優化內容載入和簡化終端使用者體驗之間達成了平衡。
- 使用者可以更快地載入到內容,因為使用者第一次開啟網站時只需要載入一部分內容。
- 網站看到更高的使用者保留,因為不斷向使用者提供內容,減少了使用者離開網站的機會。
- 網站看到較低的資源成本,因為內容只在使用者需要時才載入,而不是一次完成。
參考:
Now You See Me: How To Defer, Lazy-Load And Act With IntersectionObserver