专栏首页木子墨的前端日常懒就是生产力之图片懒加载

懒就是生产力之图片懒加载


最近负责的项目渐渐的由业务型转向营销型,营销内容越来越多,图片也就多了起来。图片一多起来问题就来了,一上来几十张图片加载起来半天都过去了,咋办?凉拌--懒加载

什么是懒加载

懒加载也叫延迟加载,本质上就是按需加载,即只有当图片dom已经在或者即将进入用户视线范围内的时候才去加载对应的dom图片。前两年流行的瀑布流其实就是按需加载的一个应用实例。

在懒加载的模型中,一个页面可以氛围三个区域如图,视图区、即将进入视图区、视图远区(即图中的不加载区)。

当一个图片在视图区或者随着屏幕滚动即将进入视图区的时候,就开始加载图片。

懒加载的处理流程

大象装冰箱,也得分三步,懒加载也一样 首先,将图片标签加入懒加载队列lazyQueue,并且给图片加载一个默认图,一般是个品牌logo或者1px*1px的空白图片做拉伸。加载默认图主要是为了看起来不那么尴尬,用户普遍尴尬耐受力强的可以不加~ 其次,添加屏幕滚动监听事件,如mousemove、wheel滚动等,实时监听懒加载队列lazyQueue中的dom位置变动。这一步算是懒加载的核心了吧。 最后,在监听器中实时判断图片dom的位置,如果已进入加载区的位置就去加载图片,加载完成后把响应的dom移出lazyQueue。 直接上代码吧,怎么感觉这么墨迹。。。

完整代码

/* global Image */
if (!Array.prototype.$remove) {
    Array.prototype.$remove = function(item) {
        if (!this.length) return
        const index = this.indexOf(item)
        if (index > -1) {
            return this.splice(index, 1)
        }
    }
}

export default (Vue, Options = {}) => {
    const isVueNext = Vue.version.split('.')[0] === '2'

    const DEFAULT_URL = Options.bgImgUrl || 'https//img.aiyoumi.com/null/2018423/101212916/20180423101212_1x1.png?height=1&width=1'

    const Init = {
        preLoad: Options.preLoad || 1.8,
        error: Options.error || DEFAULT_URL,
        loading: Options.loading || DEFAULT_URL,
        attempt: Options.attempt || 3,
        scale: Options.scale || window.devicePixelRatio,
        hasbind: false
    }

    const Listeners = []
    const imageCache = []

    const throttle = function(action, delay) {
        let timeout = null
        let lastRun = 0
        return function() {
            if (timeout) {
                return
            }
            let elapsed = (+new Date()) - lastRun
            let context = this
            let args = arguments
            let runCallback = function() {
                lastRun = +new Date()
                timeout = false
                action.apply(context, args)
            }
            if (elapsed >= delay) {
                runCallback()
            } else {
                timeout = setTimeout(runCallback, delay)
            }
        }
    }

    const _ = {
        on(el, type, func) {
            el.addEventListener(type, func)
        },
        off(el, type, func) {
            el.removeEventListener(type, func)
        }
    }

    const lazyLoadHandler = throttle(() => {
        for (let i = 0, len = Listeners.length; i < len; ++i) {
            checkCanShow(Listeners[i])
        }
    }, 300)

    const onListen = (el, start) => {
        if (start) {
            _.on(el, 'scroll', lazyLoadHandler)
            _.on(el, 'wheel', lazyLoadHandler)
            _.on(el, 'mousewheel', lazyLoadHandler)
            _.on(el, 'resize', lazyLoadHandler)
            _.on(el, 'animationend', lazyLoadHandler)
            _.on(el, 'transitionend', lazyLoadHandler)
        } else {
            Init.hasbind = false
            _.off(el, 'scroll', lazyLoadHandler)
            _.off(el, 'wheel', lazyLoadHandler)
            _.off(el, 'mousewheel', lazyLoadHandler)
            _.off(el, 'resize', lazyLoadHandler)
            _.off(el, 'animationend', lazyLoadHandler)
            _.off(el, 'transitionend', lazyLoadHandler)
        }
    }

    const checkCanShow = (listener) => {
        if (imageCache.indexOf(listener.src) > -1) {
            return setElRender(listener.el, listener.bindType, listener.src, 'loaded')
        }
        let rect = listener.el.getBoundingClientRect()
        if ((rect.top < window.innerHeight * Init.preLoad && rect.bottom >= 0) && (rect.left < window.innerWidth * Init.preLoad && rect.right >= 0)) {
            render(listener)
        }
    }

    const setElRender = (el, bindType, src, state) => {
        // 避免重复render
        let stateDone = el.getAttribute('lazy') === 'loaded'
        if (stateDone) {
            return
        }

        if (!bindType) {
            el.setAttribute('src', src)
        } else {
            el.setAttribute('style', bindType + ': url(' + src + ')')
        }

        // 默认会给图片添加有渐显效果的类名
        if (state === 'loaded' && (el.className.indexOf('animation__fade') === -1)) {
            el.className += ' animation__fade'
        }
        el.setAttribute('lazy', state)
    }

    const render = (item) => {
        if (item.attempt >= Init.attempt) {
            return false
        }
        item.attempt += 1
        loadImageAsync(item)
            .then((image) => {
                setElRender(item.el, item.bindType, item.src, 'loaded')
                imageCache.push(item.src)
                Listeners.$remove(item)
            })
            .catch((error) => {
                setElRender(item.el, item.bindType, item.error, 'error')
            })
    }

    const loadImageAsync = (item) => {
        return new Promise((resolve, reject) => {
            let image = new Image()
            image.src = item.src

            image.onload = function() {
                resolve({
                    naturalHeight: image.naturalHeight,
                    naturalWidth: image.naturalWidth,
                    src: item.src
                })
            }

            image.onerror = function() {
                reject()
            }
        })
    }

    const componentWillUnmount = (el, binding, vnode, OldVnode) => {
        if (!el) {
            return
        }

        for (let i = 0, len = Listeners.length; i < len; i++) {
            if (Listeners[i] && Listeners[i].el === el) {
                Listeners.splice(i, 1)
            }
        }

        if (Init.hasbind && Listeners.length === 0) {
            onListen(window, false)
        }
    }

    const checkElExist = (el) => {
        let hasIt = false

        Listeners.forEach((item) => {
            if (item.el === el) hasIt = true
        })

        if (hasIt) {
            return Vue.nextTick(() => {
                lazyLoadHandler()
            })
        }
        return hasIt
    }

    const addListener = (el, binding, vnode) => {
        /* if (el.getAttribute('lazy') === 'loaded') {
            return
        }
        if (checkElExist(el)) {
            return
        } */
        // 跳过不必要刷新
        if (binding.value === binding.oldValue) {
           return 
        }

        let parentEl = null
        let imageSrc = binding.value
        let imageLoading = Init.loading
        let imageError = Init.error

        if (typeof binding.value !== 'string') {
            imageSrc = binding.value.src
            imageLoading = binding.value.loading || Init.loading
            imageError = binding.value.error || Init.error
        }
        if (binding.modifiers) {
            parentEl = window.document.getElementById(Object.keys(binding.modifiers)[0])
        }

        setElRender(el, binding.arg, imageLoading, 'loading')

        vnode.context.$nextTick(() => {
            Listeners.push({
                bindType: binding.arg,
                attempt: 0,
                parentEl: parentEl,
                el: el,
                error: imageError,
                src: imageSrc
            })

            if (Listeners.length > 0 && !Init.hasbind) {
                Init.hasbind = true
                onListen(window, true)
            }
            if (parentEl) {
                onListen(parentEl, true)
            }
            lazyLoadHandler()
        })
    }

    Vue.directive('lazy', {
        bind: addListener,
        update: addListener,
        componentUpdated: lazyLoadHandler,
        unbind: componentWillUnmount
    })
}

当你看到这,我要谢谢你,这篇博客零零散散写了好几回,搁置了小一年了。。。不知道在想啥。。。什么什么都怎么写的都有点乱了。。。烂尾了。。。实在抱歉,下不为例~

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 浏览器相关--H5本地存储

    浏览器存储主要包括一下几个部分 1. cookie 2. localStorage 3. sessionStorage 4. indexDB 5. websql...

    木子墨
  • CORS跨域模型浅析及常见理解误区分析

    CORS跨域资源共享是前后端跨域十分常用的一种方案,主要依赖Access-Control-Allow(ACA)系列header来实现一种协商性的跨域交互。

    木子墨
  • Charles使用笔记

    Charles本身其实是一款十分强大且易用的代理软件,最近用的比较多,大致整理了一下自己用到的一些东西。

    木子墨
  • vue2.0 + element-ui 多级导航菜单

    自从用饿了么框架重构项目以来,遇到 很多问题,我都有一一记录下来,现在特喜欢这个框架,说实话,如果你是用了vue这个技术栈的话,一定要用饿了么的pc端框架哦,遇...

    王小婷
  • 自定义element UI的upload组件

    本文由腾讯云+社区自动同步,原文地址 https://stackoverflow.club/article/elementui_upload_custom/

    羽翰尘
  • vuejs之springboot+vue+element-ui之分页显示相关信息

    vue与springboot进行通讯:https://www.cnblogs.com/xiximayou/p/12336033.html

    绝命生
  • vue通过数据驱动实现表格行的增加与删除

    以前做明细表格的新增改查,都是需要操作dom的,但现在数据驱动,不需要了,只需要操作数据即可,相当简单

    星痕
  • vue2.0 + element-ui 实战项目-渲染表格(四)

    Element UI手册:https://cloud.tencent.com/developer/doc/1270 github地址:https://gith...

    王小婷
  • 学习Vim合并行的方法和技巧

    刚接触 Vim 会觉得它的学习曲线非常陡峭,要记住很多命令。所以这个系列的分享,不会 教你怎么配置它,而是教你怎么快速的使用它。

    砸漏
  • JavaScript设计模式之终章:重构

    模式和重构之间有着一种与生俱来的关系。从某种角度来看,设计模式的目的就是为许多重构行为提供目标。

    一粒小麦

扫码关注云+社区

领取腾讯云代金券