手摸手-100行代码实现一个功能完善的图片懒加载

手摸手-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。所以如果看官同学发现还望留言指正,景科同学在此以示感谢。

本文完整代码请点这里

如果大神同学看到此处,更希望你能留下批评指正的意见,这样景科同学才能更快的进步,下次如果你们不小心点开景科同学写的文章才不会白花花的浪费宝贵的时间,谁说不是呢😄!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,151评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,104评论 4 62
  • 当我置身于华山山顶俯瞰这世间的一切,仿佛世间的所有都如此的渺小,每一次登顶都仿佛在悬崖峭壁,如果没有围栏,便能随时...
    姚五月阅读 283评论 0 2
  • 人生匆匆忙忙的都是过客,上学时的同学,上班时的同事,活动认识的朋友,你可能会和他们中的几个成为很好的朋友,一直保持...
    你猜柠檬甜不甜阅读 301评论 0 0
  • 以前总会时不时的自怨自艾在花儿一样的年纪没有经历一场恋爱是人生中的一大缺憾,现在却在某一个瞬间突然想明白,也许上天...
    安卓妮日记阅读 152评论 0 0