手摸手-100行代碼實(shí)現(xiàn)一個功能完善的圖片懶加載

手摸手-100行代碼實(shí)現(xiàn)一個功能完善的圖片懶加載

本文相對比較初級措伐,為了節(jié)約時間特纤,請小神及其以上級別的同學(xué)直接忽略。

有同學(xué)可能會問:那么多第三方庫侥加,為什么要自己動手寫呢捧存。景科同學(xué)的想法很簡單,因?yàn)楸救四壳斑€是一個前端小白担败,只有通過不斷的寫昔穴,不斷的學(xué),在與bug的相愛相殺中才能更快速的進(jìn)步提前。在證明可行可用之后不僅可以減少項(xiàng)目的第三方依賴吗货,即便出現(xiàn)bug,解決自己代碼的bug也要比解決別人代碼的bug要好過一些狈网。話不多說宙搬,且入正題。

一. 基礎(chǔ)結(jié)構(gòu)

手摸手第一步拓哺。在第一步害淤,先把基礎(chǔ)結(jié)構(gòu)構(gòu)思搭建一下,以方便后續(xù)擼碼拓售。圖片懶加載本身就不是什么復(fù)雜的實(shí)現(xiàn)窥摄,所以基本結(jié)構(gòu)也比較簡單,無非就是初始化一下參數(shù)础淤,容一下錯崭放,綁定幾個函數(shù)哨苛,實(shí)現(xiàn)一下功能而已。具體代碼且往下看:

(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 () {
    // 全局變量币砂,所有方法在此對象上擴(kuò)展
    var Lazy = {};
    // 計時器
    var timer = null;
    // 節(jié)流延遲時間
    var delay = 150;
    // 是否開啟節(jié)流
    var debounce = true;
    // 是否開啟圖片懶加載圖片的重載建峭。解釋一下:就是圖片離開懶加載區(qū)域要把圖片狀態(tài)復(fù)原,再次進(jìn)入懶加載區(qū)域要在視覺上呈現(xiàn)懶加載效果
    // 先呵呵一下這個功能决摧,正常的我肯定想不到這么個抽風(fēng)的需求亿蒸,誰讓我曾經(jīng)碰到過一個抽風(fēng)的產(chǎn)品經(jīng)理呢,實(shí)現(xiàn)不難掌桩,這里也順便實(shí)現(xiàn)一下
    var unload = false;
    // 回掉函數(shù)
    var callback = function () {};
    // 元素相對于視窗的位置集合
    var boxRect = {};
    /**
    * 判斷目標(biāo)元素是否可見 #1
    * @param {HTMLElement} element
    * @returns {boolean}
    */
    var isHidden = function (element) {};
    /**
     * 判斷目標(biāo)元素是否進(jìn)入懶加載區(qū)域 #2
     * @param {HTMLElement} element
     * @returns {boolean}
     */
    var canLoadImg = function (element) {};
    /**
     * 節(jié)流函數(shù) #3
     */
    var onDebounceRender = function () {};
    /**
     * 始化方法边锁,外部直接調(diào)用,配置參數(shù)在此接收 #4
     * @param {Object} options
     * @param {String} options.root - 圖片滾動區(qū)域根元素選擇器
     * @param {Number} options.offset - 懶加載閾值波岛,當(dāng)沒有【上下左右】4個值時將以此為準(zhǔn)
     * @param {Number} options.offsetTop - 懶加載閾值【上】
     * @param {Number} options.offsetRight - 懶加載閾值【右】
     * @param {Number} options.offsetBottom - 懶加載閾值【下】
     * @param {Number} options.offsetLeft - 懶加載閾值【左】
     * @param {Boolean} options.debounce - 是否開啟函數(shù)節(jié)流
     * @param {Number} options.delay - 函數(shù)節(jié)流時間閾值
     * @param {Boolean} options.unload - 圖片重載
     * @param {Function} options.callback - 懶加載操作完成時的回掉函數(shù)
     */
    Lazy.init = function(options) {};
    /**
     * 懶加載實(shí)現(xiàn) #5
     * @param {HTMLElement} element
     */
    Lazy.render = function(element) {};
    /**
     * 不滿足懶加載條件時銷毀 #6
     */
    Lazy.destroy = function() {};
    // 返回
    return Lazy;
});

由于項(xiàng)目本身并不復(fù)雜茅坛,所以基礎(chǔ)的結(jié)構(gòu)也比較簡單,剩下的幾步我們只需要手摸手去做填空題(#1则拷、#2贡蓖、#3、#4煌茬、#5斥铺、#6)就好了。so easy坛善,let`s go晾蜘!

二. 功能函數(shù)實(shí)現(xiàn)

手摸手第二步。在第二步我們先把#1浑吟、#2、#3三個功能函數(shù)實(shí)現(xiàn)一下耗溜。

首先是#1函數(shù)isHidden的實(shí)現(xiàn)组力。

/**
 * 判斷目標(biāo)元素是否可見
 * 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函數(shù)canLoadImg的實(shí)現(xiàn)。這兒用到了Element.getBoundingClientRect()方法返回元素的大小及其相對于視口的位置抖拴。關(guān)于Element.getBoundingClientRect()的信息請點(diǎn)擊傳送陣了解更多燎字。

/**
 * 判斷目標(biāo)元素是否進(jìn)入懶加載區(qū)域
 * 此函數(shù)依賴isHidden函數(shù)和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函數(shù)onDebounceRender的實(shí)現(xiàn)。由于這里對節(jié)流函數(shù)沒什么特殊需求阿宅,所以實(shí)現(xiàn)的比較簡單候衍,如果看官同學(xué)需要完整的debounce函數(shù),請點(diǎn)擊lodash/debounce.js了解更多洒放。

/**
 * 節(jié)流函數(shù)
 * 此函數(shù)依賴Lazy.render蛉鹿、debounce、timer往湿、delay
 */
var onDebounceRender = function () {
    if (!debounce) {
        Lazy.render();
    } else {
        clearTimeout(timer);
        timer = setTimeout(function () {
            Lazy.render();
            timer = null;
        }, delay);
    }
};

三. 初始化函數(shù)

手摸手第三步妖异。我們來實(shí)現(xiàn)一下Lazy.init初始化函數(shù)惋戏。這個函數(shù)的作用就是接收參數(shù)、綁定事件處理函數(shù)他膳,所以更簡單响逢。

/**
 * 始化方法,外部直接調(diào)用棕孙,配置參數(shù)在此接收
 * @param {Object} options
 * @param {String} options.root - 圖片滾動區(qū)域根元素選擇器
 * @param {Number} options.offset - 懶加載閾值舔亭,當(dāng)沒有上下左右有4個值時將以此為準(zhǔn)
 * @param {Number} options.offsetTop - 懶加載閾值【上】
 * @param {Number} options.offsetRight - 懶加載閾值【右】
 * @param {Number} options.offsetBottom - 懶加載閾值【下】
 * @param {Number} options.offsetLeft - 懶加載閾值【左】
 * @param {Boolean} options.debounce - 是否開啟函數(shù)節(jié)流
 * @param {Number} options.delay - 函數(shù)節(jié)流時間閾值
 * @param {Boolean} options.unload - 圖片重載
 * @param {Function} options.callback - 懶加載操作完成時的回掉函數(shù)
 */
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;
    // 懶加載區(qū)域,寫的雖然有點(diǎn)長但是不難理解
    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)
    };
    // 這里提前調(diào)用一次蟀俊,因?yàn)槿绻鹍ebounce為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);
    }
};

四. 核心方法實(shí)現(xiàn)

手摸手第四步。在第四步我們來完成Lazy.render函數(shù)的實(shí)現(xiàn)欧漱。這個函數(shù)也是本項(xiàng)目的核心方法职抡,所有的懶加載實(shí)現(xiàn)都是在此處完成。思路不復(fù)雜误甚,所以實(shí)現(xiàn)起來也比較簡單缚甩。

/**
 * 懶加載實(shí)現(xiàn)
 * @param {HTMLElement} element
 */
Lazy.render = function (element) {
    // dom結(jié)構(gòu)約定:img標(biāo)簽要有[data-lazy]自定義屬性,需要設(shè)置背景的標(biāo)簽需要有[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];
        // 目標(biāo)元素在懶加載區(qū)域和不在懶加載區(qū)域
        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');
            // 懶加載完成觸發(fā)回掉
            callback(node, 'load');
        } else {
            // 當(dāng)圖片不在懶加載區(qū)域時做重載處理
            var placeholder = node.getAttribute('data-lazy-placeholder');
            if (unload && placeholder !== null) {
                node.src = placeholder;
                // 移除[data-lazy-placeholder]自定義屬性
                node.removeAttribute('data-lazy-placeholder');
                // 重載完成觸發(fā)回掉
                callback(node, 'unload');
            }
        }
    }
    // 如果沒有懶加載的元素,銷毀Lazy.init添加的事件
    if (len === 0) {
        Lazy.destroy();
    }
};

五. 解綁方法實(shí)現(xiàn)

手摸手第五步冈钦。這一步更簡單郊丛,話不多說直接看代碼。

/**
 * 不滿足懶加載條件時移除綁定的事件瞧筛,重置定時器引用
 */
Lazy.destroy = function () {
    if (document.removeEventListener) {
        global.removeEventListener('scroll', onDebounceRender);
    } else {
        global.detachEvent('onscroll', onDebounceRender);
    }
    clearTimeout(timer);
};

六. 結(jié)語

由于景科同學(xué)剛開始寫博文厉熟,語文老師走的又早(是真的早)??,文筆難免摳腳较幌,不足之處還望各位看官同學(xué)多多包含揍瑟。本項(xiàng)目是景科同學(xué)自寫自測,雖然比較簡單乍炉,但是不保證沒有隱藏的bug绢片。所以如果看官同學(xué)發(fā)現(xiàn)還望留言指正,景科同學(xué)在此以示感謝岛琼。

本文完整代碼請點(diǎn)這里底循。

如果大神同學(xué)看到此處,更希望你能留下批評指正的意見槐瑞,這樣景科同學(xué)才能更快的進(jìn)步熙涤,下次如果你們不小心點(diǎn)開景科同學(xué)寫的文章才不會白花花的浪費(fèi)寶貴的時間,誰說不是呢??!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末灭袁,一起剝皮案震驚了整個濱河市猬错,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌茸歧,老刑警劉巖倦炒,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異软瞎,居然都是意外死亡逢唤,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門涤浇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鳖藕,“玉大人,你說我怎么就攤上這事只锭≈鳎” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵蜻展,是天一觀的道長喉誊。 經(jīng)常有香客問我,道長纵顾,這世上最難降的妖魔是什么伍茄? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮施逾,結(jié)果婚禮上敷矫,老公的妹妹穿的比我還像新娘。我一直安慰自己汉额,他們只是感情好曹仗,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蠕搜,像睡著了一般怎茫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上讥脐,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天遭居,我揣著相機(jī)與錄音啼器,去河邊找鬼旬渠。 笑死,一個胖子當(dāng)著我的面吹牛端壳,可吹牛的內(nèi)容都是我干的告丢。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼损谦,長吁一口氣:“原來是場噩夢啊……” “哼岖免!你這毒婦竟也來了岳颇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤颅湘,失蹤者是張志新(化名)和其女友劉穎话侧,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闯参,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瞻鹏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了鹿寨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片新博。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖脚草,靈堂內(nèi)的尸體忽然破棺而出赫悄,到底是詐尸還是另有隱情,我是刑警寧澤馏慨,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布埂淮,位于F島的核電站,受9級特大地震影響熏纯,放射性物質(zhì)發(fā)生泄漏同诫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一樟澜、第九天 我趴在偏房一處隱蔽的房頂上張望误窖。 院中可真熱鬧,春花似錦秩贰、人聲如沸霹俺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽丙唧。三九已至,卻和暖如春觅玻,著一層夾襖步出監(jiān)牢的瞬間想际,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工溪厘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留胡本,地道東北人。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓畸悬,卻偏偏與公主長得像侧甫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,754評論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫披粟、插件咒锻、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,059評論 4 62
  • 當(dāng)我置身于華山山頂俯瞰這世間的一切,仿佛世間的所有都如此的渺小守屉,每一次登頂都仿佛在懸崖峭壁惑艇,如果沒有圍欄,便能隨時...
    姚五月閱讀 280評論 0 2
  • 人生匆匆忙忙的都是過客兢卵,上學(xué)時的同學(xué),上班時的同事绪颖,活動認(rèn)識的朋友秽荤,你可能會和他們中的幾個成為很好的朋友,一直保持...
    你猜檸檬甜不甜閱讀 296評論 0 0
  • 以前總會時不時的自怨自艾在花兒一樣的年紀(jì)沒有經(jīng)歷一場戀愛是人生中的一大缺憾,現(xiàn)在卻在某一個瞬間突然想明白牍氛,也許上天...
    安卓妮日記閱讀 145評論 0 0