圖片懶加載踩坑

原文地址:圖片懶加載踩坑javascript

原理

對網頁加載速度影響最大的就是圖片,一張普通的圖片可能會有好幾 M 的大小,當圖片不少時,網頁的加載速度變得很緩慢。html

爲了優化網頁性能以及用戶體驗,咱們對圖片進行懶加載vue

懶加載是一種對網頁性能優化的方式,它的原理是優先加載在可視區域內的圖片,而不一次性加載因此圖片。當瀏覽器滾動,圖片進入可視區時再去加載圖片。經過設置圖片的 src 屬性來讓瀏覽器發起圖片的請求。當這個屬性爲空或者沒有時,就不會發送請求。java

實現

此文所涉及的懶加載皆是在垂直方向上的滾動加載,橫向滾動暫不考慮。git

懶加載的實現主要是判斷當前圖片是否到了可視區域這一核心邏輯。咱們先來整理一下實現思路:github

  1. 拿到全部的圖片 img dom
  2. 遍歷每一個圖片判斷當前圖片是否到了可視區範圍內。
  3. 若是到了就設置圖片的 src 屬性。
  4. 綁定 window 的 scroll 事件,對其進行事件監聽。

HTML 結構

<div class="container">
    <div class="img-area">
        <img id="first" data-src="./img/ceng.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/data.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/huaji.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/liqi1.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/liqi2.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/steve-jobs.jpg" alt="">
    </div>
</div>
複製代碼

此時 img 標籤是沒有 src 屬性的,咱們把真實的圖片地址放在一個屬性裏,這裏咱們使用 HTML5 的 data 屬性,將真實地址放在自定義的 data-src 中。瀏覽器

判斷圖片是否進入了可視區域

這一邏輯有兩種方法,聽我娓娓道來。性能優化

方法一app

第一種方法咱們經過計算該圖片距離 document 頂部的高度是否小於當前可視區域相對於 document 頂部高度來判斷。dom

可視區域相對於 document 頂部高度的計算方法:

const clientHeight = document.documentElement.clientHeight; // 視口高度,也就是窗口的高度。
const scrollHeight = document.documentElement.scrollTop + clientHeight; // 滾動條偏移文檔頂部的高度(也就是文檔從頂部開始到可視區域被抹去的高度) + 視口高度
複製代碼

畫了一張圖方便理解:

而後就是計算該圖片距離文檔頂部的高度。有兩種方法,第一種方法是經過元素的 offsetTop 屬性來計算。從上圖咱們瞭解到元素的 offsetTop 屬性是相對於一個 position非 static 的祖先元素,也就是 child.offsetParent 。同時須要將祖先元素的 border 考慮在內,咱們經過child.offsetParent.clientTop能夠拿到邊框厚度。

由此咱們獲得元素距離文檔頂部的高度的計算方法:

function getTop(el, initVal) {
    let top = el.offsetTop + initVal;
    if (el.offsetParent !== null) {
        top += el.offsetParent.clientTop;
        return getTop(el.offsetParent, top);
    } else {
        return top;
    }
}
複製代碼

這裏的這個方法使用了 尾遞歸調用 。能夠提升遞歸性能。固然這裏也能夠用循環來實現:

function getTop(el) {
    let top = el.offsetTop;
    var parent = el.offsetParent;
    while(parent !== null) {
        top += parent.offsetTop + parent.clientTop;
        parent = parent.offsetParent;
    }
    return top;
}
複製代碼

第二種方法是使用 element.getBoundingClientRect() API 直接獲得 top 值。

getBoundingClientRect 的返回值以下圖:

var first  = document.getElementById('first');
getTop(first, 0);  // 130
console.log(first.getBoundingClientRect().top); // 130
複製代碼

因而咱們獲得判斷方法:

function inSight(el) {
    const clientHeight = document.documentElement.clientHeight;
    const scrollHeight = document.documentElement.scrollTop + clientHeight;
    // 方法一
    return getTop(el, 0) < scrollHeight;
    // 方法二
    // return el.getBoundingClientRect().top < clientHeight;
}
複製代碼

接下來就是對每一個圖片進行判斷和賦值。

function loadImg(el) {
    if (!el.src) {
        el.src = el.dataset.src;
    }
}

function checkImgs() {
    const imgs = document.getElementsByTagName('img');
    Array.from(imgs).forEach(el => {
        if (inSight(el)) {
            loadImg(el);
        }
    })
    console.log(count++);
}
複製代碼

最後給 window 綁定 onscroll 事件以及 onload 事件:

window.addEventListener('scroll', checkImgs, false);
window.onload = checkImgs;
複製代碼

咱們知道相似 scrollresize 這樣的事件瀏覽器可能在很短的時間內觸發不少次,爲了提升網頁性能,咱們須要一個節流函數來控制函數的屢次觸發,在一段時間內(如 500ms)只執行一次回調。

/** * 持續觸發事件,每隔一段時間,只執行一次事件。 * @param fun 要執行的函數 * @param delay 延遲時間 * @param time 在 time 時間內必須執行一次 */
function throttle(fun, delay, time) {
    var timeout;
    var previous = +new Date();
    return function () {
        var now = +new Date();
        var context = this;
        var args = arguments;
        clearTimeout(timeout);
        if (now - previous >= time) {
            fun.apply(context, args);
            previous = now;
        } else {
            timeout = setTimeout(function () {
                fun.apply(context, args);
            }, delay);
        }
    }
}
window.addEventListener('scroll', throttle(checkImgs, 200, 1000), false);
複製代碼

方法二

HTML5 有一個新的 IntersectionObserver API,它能夠自動觀察元素是否可見。

主要用法:

var observer = new IntersectionObserver(callback, option);

// 開始觀察
observer.observe(document.getElementById('first'));

// 中止觀察
observer.unobserve(document.getElementById('first'));

// 關閉觀察器
observer.disconnect();
複製代碼

目標的可見性發生變化時就會調用觀察器的 callback。

function callback(changes: IntersectionObserverEntry[]) {
    console.log(changes[0])
}

// IntersectionObserverEntry
{
    time: 29.499999713152647,
    intersectionRatio: 1,
    boundingClientRect: DOMRectReadOnly {
        bottom: 144,
        height: 4,
        left: 289,
        right: 293,
        top: 140,
        width: 4,
        x: 289,
        y: 140
    },
    intersectionRect: DOMRectReadOnly,
    isIntersecting: true,
    rootBounds: DOMRectReadOnly,
    target: img#first
}
複製代碼

詳細釋義:

  • time: 可見性發生變化的時間,是一個高精度時間戳,單位爲毫秒
  • intersectionRatio: 目標元素的可見比例,即 intersectionRect 佔 boundingClientRect 的比例,徹底可見時爲 1 ,徹底不可見時小於等於 0
  • boundingClientRect: 目標元素的矩形區域的信息
  • intersectionRect: 目標元素與視口(或根元素)的交叉區域的信息
  • rootBounds: 根元素的矩形區域的信息,getBoundingClientRect() 方法的返回值,若是沒有根元素(即直接相對於視口滾動),則返回 null
  • isIntersecting: 是否進入了視口,boolean 值
  • target: 被觀察的目標元素,是一個 DOM 節點對象

使用 IntersectionObserver 實現圖片懶加載:

function query(tag) {
    return Array.from(document.getElementsByTagName(tag));
}
var observer = new IntersectionObserver(
    (changes) => {
        changes.forEach((change) => {
            if (change.intersectionRatio > 0) {
                var img = change.target;
                img.src = img.dataset.src;
                observer.unobserve(img);
            }
        })
    }
)
query('img').forEach((item) => {
    observer.observe(item);
})
複製代碼

完整代碼見 github

完 :)

相關文章
相關標籤/搜索