手摸手-100行程式碼實現一個功能完善的圖片懶載入

景科同學發表於2018-01-22

手摸手-100行程式碼實現一個功能完善的圖片懶載入

本文相對比較初級,為了節約時間,請小神及其以上級別的同學直接忽略。

有同學可能會問:那麼多第三方庫,為什麼要自己動手寫呢。景科同學的想法很簡單,因為本人目前還是一個前端小白,只有通過不斷的寫,不斷的學,在與bug的相愛相殺中才能更快速的進步。在證明可行可用之後不僅可以減少專案的第三方依賴,即便出現bug,解決自己程式碼的bug也要比解決別人程式碼的bug要好過一些。話不多說,且入正題。

一. 基礎結構

手摸手第一步。在第一步,先把基礎結構構思搭建一下,以方便後續擼碼。圖片懶載入本身就不是什麼複雜的實現,所以基本結構也比較簡單,無非就是初始化一下引數,容一下錯,繫結幾個函式,實現一下功能而已。具體程式碼且往下看:

(function (global, factory) {
    if (typeof exports === 'object' && typeof module !== 'undefined') {
        module.exports = factory(global)
    } else if (typeof define === 'function' && define.amd) {
        define(factory)
    } else {
        global.Lazy = factory(global)
    }
})(this, function () {
    // 全域性變數,所有方法在此物件上擴充套件
    var Lazy = {};
    // 計時器
    var timer = null;
    // 節流延遲時間
    var delay = 150;
    // 是否開啟節流
    var debounce = true;
    // 是否開啟圖片懶載入圖片的過載。解釋一下:就是圖片離開懶載入區域要把圖片狀態復原,再次進入懶載入區域要在視覺上呈現懶載入效果
    // 先呵呵一下這個功能,正常的我肯定想不到這麼個抽風的需求,誰讓我曾經碰到過一個抽風的產品經理呢,實現不難,這裡也順便實現一下
    var unload = false;
    // 回掉函式
    var callback = function () {};
    // 元素相對於視窗的位置集合
    var boxRect = {};
    /**
    * 判斷目標元素是否可見 #1
    * @param {HTMLElement} element
    * @returns {boolean}
    */
    var isHidden = function (element) {};
    /**
     * 判斷目標元素是否進入懶載入區域 #2
     * @param {HTMLElement} element
     * @returns {boolean}
     */
    var canLoadImg = function (element) {};
    /**
     * 節流函式 #3
     */
    var onDebounceRender = function () {};
    /**
     * 始化方法,外部直接呼叫,配置引數在此接收 #4
     * @param {Object} options
     * @param {String} options.root - 圖片滾動區域根元素選擇器
     * @param {Number} options.offset - 懶載入閾值,當沒有【上下左右】4個值時將以此為準
     * @param {Number} options.offsetTop - 懶載入閾值【上】
     * @param {Number} options.offsetRight - 懶載入閾值【右】
     * @param {Number} options.offsetBottom - 懶載入閾值【下】
     * @param {Number} options.offsetLeft - 懶載入閾值【左】
     * @param {Boolean} options.debounce - 是否開啟函式節流
     * @param {Number} options.delay - 函式節流時間閾值
     * @param {Boolean} options.unload - 圖片過載
     * @param {Function} options.callback - 懶載入操作完成時的回掉函式
     */
    Lazy.init = function(options) {};
    /**
     * 懶載入實現 #5
     * @param {HTMLElement} element
     */
    Lazy.render = function(element) {};
    /**
     * 不滿足懶載入條件時銷燬 #6
     */
    Lazy.destroy = function() {};
    // 返回
    return Lazy;
});
複製程式碼

由於專案本身並不複雜,所以基礎的結構也比較簡單,剩下的幾步我們只需要手摸手去做填空題(#1、#2、#3、#4、#5、#6)就好了。so easy,let`s go!

二. 功能函式實現

手摸手第二步。在第二步我們先把#1、#2、#3三個功能函式實現一下。

首先是#1函式isHidden的實現。

/**
 * 判斷目標元素是否可見
 * https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/offsetParent
 * @param {HTMLElement} element
 * @returns {boolean}
 */
var isHidden = function (element) {
    return element.offsetParent === null;
};
複製程式碼

接下來是#2函式canLoadImg的實現。這兒用到了Element.getBoundingClientRect()方法返回元素的大小及其相對於視口的位置。關於Element.getBoundingClientRect()的資訊請點選傳送陣瞭解更多。

/**
 * 判斷目標元素是否進入懶載入區域
 * 此函式依賴isHidden函式和boxRect全域性變數
 * @param {HTMLElement} element
 * @returns {boolean}
 */
var canLoadImg = function (element) {
    if (isHidden(element)) return false;
    var eleRect = element.getBoundingClientRect();
    return (eleRect.top <= boxRect.b && eleRect.right >= boxRect.l && eleRect.bottom >= boxRect.t && eleRect.left <= boxRect.r);
};
複製程式碼

最後是#3函式onDebounceRender的實現。由於這裡對節流函式沒什麼特殊需求,所以實現的比較簡單,如果看官同學需要完整的debounce函式,請點選lodash/debounce.js瞭解更多。

/**
 * 節流函式
 * 此函式依賴Lazy.render、debounce、timer、delay
 */
var onDebounceRender = function () {
    if (!debounce) {
        Lazy.render();
    } else {
        clearTimeout(timer);
        timer = setTimeout(function () {
            Lazy.render();
            timer = null;
        }, delay);
    }
};
複製程式碼

三. 初始化函式

手摸手第三步。我們來實現一下Lazy.init初始化函式。這個函式的作用就是接收引數、繫結事件處理函式,所以更簡單。

/**
 * 始化方法,外部直接呼叫,配置引數在此接收
 * @param {Object} options
 * @param {String} options.root - 圖片滾動區域根元素選擇器
 * @param {Number} options.offset - 懶載入閾值,當沒有上下左右有4個值時將以此為準
 * @param {Number} options.offsetTop - 懶載入閾值【上】
 * @param {Number} options.offsetRight - 懶載入閾值【右】
 * @param {Number} options.offsetBottom - 懶載入閾值【下】
 * @param {Number} options.offsetLeft - 懶載入閾值【左】
 * @param {Boolean} options.debounce - 是否開啟函式節流
 * @param {Number} options.delay - 函式節流時間閾值
 * @param {Boolean} options.unload - 圖片過載
 * @param {Function} options.callback - 懶載入操作完成時的回掉函式
 */
Lazy.init = function (options) {
    options = options || {};
    global = document.querySelector(options.root) || global;
    debounce = options.debounce !== false;
    delay = options.delay || delay;
    unload = options.unload || unload;
    callback = options.callback || callback;
    // 懶載入區域,寫的雖然有點長但是不難理解
    boxRect = {
        t: 0 - (options.offsetTop || options.offset || 0),
        r: (global.innerWidth || document.documentElement.clientWidth) + (options.offsetRight || options.offset || 0),
        b: (global.innerHeight || document.documentElement.clientHeight) + (options.offsetBottom || options.offset || 0),
        l: 0 - (options.offsetLeft || options.offset || 0)
    };
    // 這裡提前呼叫一次,因為如果debounce為true,load之後會有最低250ms的延遲
    Lazy.render();
    // 繫結事件
    if (global.addEventListener) {
        global.addEventListener('load', onDebounceRender, false);
        global.addEventListener('scroll', onDebounceRender, false);
    } else {
        global.attachEvent('onload', onDebounceRender);
        global.attachEvent('onscroll', onDebounceRender);
    }
};
複製程式碼

四. 核心方法實現

手摸手第四步。在第四步我們來完成Lazy.render函式的實現。這個函式也是本專案的核心方法,所有的懶載入實現都是在此處完成。思路不復雜,所以實現起來也比較簡單。

/**
 * 懶載入實現
 * @param {HTMLElement} element
 */
Lazy.render = function (element) {
    // dom結構約定:img標籤要有[data-lazy]自定義屬性,需要設定背景的標籤需要有[data-lazy-background]自定義屬性
    var nodes = (element || document).querySelectorAll('[data-lazy], [data-lazy-background]');
    var len = nodes.length;
    for (var i = 0; i < len; i++) {
        var node = nodes[i];
        // 目標元素在懶載入區域和不在懶載入區域
        if (canLoadImg(node)) {
            // 懶載入圖片需要過載,懶載入之前先將佔點陣圖片儲存
            if (unload && node.src && !node.getAttribute('data-lazy-placeholder')) {
                node.setAttribute('data-lazy-placeholder', node.src);
            }
            // 圖片的懶載入
            var src = node.getAttribute('data-lazy');
            if (src !== null && node.src !== src) {
                node.src = src;
            }
            // 背景的懶載入
            var bgUrl = node.getAttribute('data-lazy-background');
            if (bgUrl !== null && node.style.backgroundImage !== bgUrl) {
                node.style.backgroundImage = 'url(' + bgUrl + ')';
            }
            // 如果圖片不需要過載,懶載入完成移除[data-lazy]自定義屬性
            if (!unload) {
                node.removeAttribute('data-lazy');
            }
            // 懶載入完成移除[data-lazy-background]自定義屬性
            node.removeAttribute('data-lazy-background');
            // 懶載入完成觸發回掉
            callback(node, 'load');
        } else {
            // 當圖片不在懶載入區域時做過載處理
            var placeholder = node.getAttribute('data-lazy-placeholder');
            if (unload && placeholder !== null) {
                node.src = placeholder;
                // 移除[data-lazy-placeholder]自定義屬性
                node.removeAttribute('data-lazy-placeholder');
                // 過載完成觸發回掉
                callback(node, 'unload');
            }
        }
    }
    // 如果沒有懶載入的元素,銷燬Lazy.init新增的事件
    if (len === 0) {
        Lazy.destroy();
    }
};
複製程式碼

五. 解綁方法實現

手摸手第五步。這一步更簡單,話不多說直接看程式碼。

/**
 * 不滿足懶載入條件時移除繫結的事件,重置定時器引用
 */
Lazy.destroy = function () {
    if (document.removeEventListener) {
        global.removeEventListener('scroll', onDebounceRender);
    } else {
        global.detachEvent('onscroll', onDebounceRender);
    }
    clearTimeout(timer);
};
複製程式碼

六. 結語

由於景科同學剛開始寫博文,語文老師走的又早(是真的早)?,文筆難免摳腳,不足之處還望各位看官同學多多包含。本專案是景科同學自寫自測,雖然比較簡單,但是不保證沒有隱藏的bug。所以如果看官同學發現還望留言指正,景科同學在此以示感謝。

本文完整程式碼請點這裡

如果大神同學看到此處,更希望你能留下批評指正的意見,這樣景科同學才能更快的進步,下次如果你們不小心點開景科同學寫的文章才不會白花花的浪費寶貴的時間,誰說不是呢?!

相關文章