图片延迟加载代码分析

最近在写在手机上运行的web项目,固然要考虑到图片加载的问题,以尽可能的减少图片的加载量,在这里,就用到了图片延迟加载技术。
在github上找了一些别人写的工具拿来分析,发现其中有几个新的技术点,是值得学习借鉴的。

imagelazyloader##

maxzhang

图片延迟加载
阅读代码笔记

 //闭包,默认传入为window,参数变量名又为window,可以改为其它关键字
 (function(window) {
    //判断是否android系统,主要为后面是否有动画的判断量
    var isAndroid = /Android/i.test(window.navigator.userAgent),
        //创建一个空的div对象,显拿到style属性
        dummyStyle = document.createElement('div').style,
        //获取css前缀类型,例如webkit,moz,ms...
        //在chrome下做试验,vendor为'',则为chrome支持 没有前缀的transform
        vendor = (function() {
            var vendors = 't,webkitT,MozT,msT,OT'.split(','),
                t,
                i = 0,
                l = vendors.length;

            for (; i < l; i++) {
                t = vendors[i] + 'ransform';
                if (t in dummyStyle) {
                    return vendors[i].substr(0, vendors[i].length - 1);
                }
            }

            return false;
        })(),
        //获取不同平台的 transitionEnd 事件名称
        //,当CSS 3的animation动画执行结束时,触发webkitAnimationEnd事件,当CSS 3的transition动画执行结束时,触发webkitTransitionEnd事件
        //现在最最的opare浏览器内核也是webkit的,所以 -o-应该可以不考虑了
        transitionEndEvent = (function() {
            if (vendor == 'webkit' || vendor === 'O') {
                return vendor.toLowerCase() + 'TransitionEnd';
            }
            return 'transitionend';
        }()),
        //监听动画事件,参数分别是 节点目标,动事时间,及callback
        listenTransition = function(target, duration, callbackFn) {
            var me = this,
                //清除事件监听
                clear = function() {
                    //测试是否起定时,有则清除
                    if (target.transitionTimer) clearTimeout(target.transitionTimer);
                    //设置定时器为空
                    target.transitionTimer = null;
                    //移除对transitionEnd Event的监听
                    target.removeEventListener(transitionEndEvent, handler, false);
                },
                //监听方法体
                handler = function() {
                    //首选清除监听事件
                    clear();
                    //执行callback方法
                    if (callbackFn) callbackFn.call(me);
                };
            //初始化清初事件监听
            clear();
            //监听target的transitionEndEvent事件,执行callback
            target.addEventListener(transitionEndEvent, handler, false);
            //动画时间+100ms后执行callback
            target.transitionTimer = setTimeout(handler, duration + 100);
        },
        //代理,主要实现的是this对象的代理
        proxy = function(fn, scope) {
            return function() {
                return fn.apply(scope, arguments);
            };
        };
    //进入正题
    //
    var ImageLazyLoader = function(config) {
        //设置config 的值,默认为一空对象
        config = config || {};
        //设置配置对象值为类属性
        for (var o in config) {
            this[o] = config[o];
        }
        //设置ct类属性值,body
        this.ct = document.body;
        //执行_onScroll方法,this为当前类实例
        //返回一新的fn,传入的fn的this为当前类对象,以防止在下面的监听事件中,改变this
        this._onScroll_ = proxy(this._onScroll, this);
        //坚挺window 默认 scroll事件
        window.addEventListener('scroll', this._onScroll_, false);
        //设置maxScrollY默认值
        this.maxScrollY = 0;
        //是android的话,不使用渐出动画
        if (isAndroid) { // 在android下,opacity动画效果比较差
            this.useFade = false;
        }

        this.elements = [];
        this.lazyElements = {};
        this.scan(this.ct);

        this._onPageShow_ = proxy(this._onPageShow, this);
        //监听window事件,pageshow 事件
        //第一个事件就是pageshow,这个事件在页面显示时触发,
        //无论页面是否来自bfcache。在重新加载页面中,
        //pageshow会在load事件触发后触发;而对于bfcache中的页面,
        //pageshow会在页面状态完全恢复的那一刻触发。另外要注意的是,
        //虽然这个事件的目标是document,但必须将其事件处理程序添加到window。
         window.addEventListener('pageshow', this._onPageShow_, false);
    };

    ImageLazyLoader.prototype = {
        range: 200,
        //图片地址,在img标签中的属性标签
        realSrcAttribute: 'data-src',
        //是否使用渐变动画。默认为ture
        useFade: true,
        //当显示页面时执行
        _onPageShow: function(e) {
            //“往返缓存”(back-forward cache,或bfcache),
            //可以在用户使用浏览器的“后退”和“前进”按钮时加快页面的转换速度。
            //这个缓存中不仅保存着页面数据,还保存了DOM和JavaScript的状态;
            //pageshow事件的event对象还包含一个名为persisted的布尔值属性。
            //如果页面中保存在了bfcache中,则这个属性的值为true;否则,这个属性的值为false。
            if (e.persisted) {
                //重新设置maxscroll的值
                this.maxScrollY = 0;
                //重新加载节点
                this.scan(this.ct);
            }
        },
        //当滚动时,判断当前值是否大于最大maxscrollY,决定是否触发scrollAc
        _onScroll: function() {
            var scrollY = this.getScrollY();
            if (scrollY > this.maxScrollY) {
                this.maxScrollY = scrollY;
                this._scrollAction();
            }
        },
        //获取当前页面的位置
        getScrollY: function() {
            //经测验,两个属性均可
            return window.pageYOffset || window.scrollY;
        },
        //滚动页面时,触发,主要是将到显示区的标签装入lazyEls中
        _scrollAction: function() {
            //清除定时器
            clearTimeout(this.lazyLoadTimeout);
            
            this.elements = this.elements.filter(function(img) {
                //getBoundingClientRect用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。
                //document.documentElement.clientTop;  // 非IE为0,IE为2
                //如果图片的位置在 将要进入可视范围内时
                if ((this.range + window.innerHeight) >= (img.getBoundingClientRect().top - document.documentElement.clientTop)) {
                    //拿到图片地址
                    var realSrc = img.getAttribute(this.realSrcAttribute);
                    
                    if (realSrc) {
                        //装到lazyels数组中
                        if (this.lazyElements[realSrc]) {
                            this.lazyElements[realSrc].push(img);
                        } else {
                            this.lazyElements[realSrc] = [img];
                        }
                    }
                    //已经装入库中,过滤掉
                    return false;
                }
                //不符合条件的,从库中去除
                return true;
            }, this);
            //设置定时器,根据平台不同,执行平率有所改变
            this.lazyLoadTimeout = setTimeout(proxy(this._loadImage, this), isAndroid ? 500 : 0);
        },
        //
        _loadImage: function() {
            var img, realSrc, imgs;
            //遍历lazyEls中的数组对够用
            for (realSrc in this.lazyElements) {
                //拿到一个数组对象
                imgs = this.lazyElements[realSrc];
                //拿到第一个图片对象,并从数组中移除
                img = imgs.shift();
                //如果图片数组中没有数据,则删除此数据对象
                if (imgs.length === 0) {
                    delete this.lazyElements[realSrc];
                }
                //检测img对象的load事件
                img.addEventListener('load', proxy(this._onImageLoad, this), false);
                //设置img的src值
                if (img.src != realSrc) {
                    this._setImageSrc(img, realSrc);
                } else {
                    this._onImageLoad(img);
                }
            }
        },
        //加载图片时,触发
        _onImageLoad: function(e) {
            var me = this,
                img = e.target || e,
                realSrc = img.getAttribute(me.realSrcAttribute),
                //再次拿到图片数组对象
                imgs = me.lazyElements[realSrc];
            //显示图片
            me._showImage(img);

            if (imgs) {
                //遍历所有数组并加载显示每一个图片
                imgs.forEach(function(i) {
                    me._setImageSrc(i, realSrc);
                    me._showImage(i);
                });
                //然后删除这个图片数据对象
                delete me.lazyElements[realSrc];
            }
        },
        //设置图片的地址,如果不是android的话,设置图片透明度为0
        _setImageSrc: function(img, realSrc) {
            if (this.useFade) {
                img.style.opacity = '0';
            }
            img.src = realSrc;
        },
        //显示图片,设置图片已加载标记
        //
        _showImage: function(img) {
            var me = this,
                cb = function() {
                    img.setAttribute('data-lazy-load-completed', '1');
                    if (me.onImageLoad) me.onImageLoad(img);
                };
            if (me.useFade) {
                img.style[vendor + 'Transition'] = 'opacity 200ms';
                img.style.opacity = 1;
                //监听动画事件
                listenTransition(img, 200, cb);
            } else {
                cb();
            }
        },
        // 设置图片区域
        scan: function(ct) {
            var imgs;
            ct = ct || document.body;
            //找到所有关联图片
            imgs = ct.querySelectorAll('img[' + this.realSrcAttribute + ']') || [];
            //
            //为什么这一步??相当于转换为数据格式?
            imgs = Array.prototype.slice.call(imgs, 0);
            //过滤img标签
            imgs = imgs.filter(function(img) {
                //标签没有img或是已加载过的,remove
                if (this.elements.indexOf(img) != -1 || img.getAttribute('data-lazy-load-completed') == '1') {
                    return false;
                }
                return true;
            }, this);
            //新加载的,合并到elements中
            this.elements = this.elements.concat(imgs);
            //执行加载方法
            this._scrollAction();
        },
        //移除scroll,pageshow事件,清空数据
        destroy: function() {
            if (!this.destroyed) {
                this.destroyed = true;
                window.removeEventListener('scroll', this._onScroll_, false);
                window.removeEventListener('pageshow', this._onPageShow_, false);
                this.elements = this.lazyElements = null;
            }
        }
    };

    dummyStyle = null;

    window.ImageLazyLoader = ImageLazyLoader;
//这里的window 可以改为this也可
})(window);

作者的代码只是对img标签做了监控,本人建议对非img标签也做监控,然后以扩展使用范围(本人有时会用背景图片显示图片,以自动比例宽高)。

你可能感兴趣的:(图片延迟加载代码分析)