本文在github作了收录 github.com/Michael-lzg…html
demo源码地址 github.com/Michael-lzg…vue
在类电商类项目,每每存在大量的图片,如 banner 广告图,菜单导航图,美团等商家列表头图等。图片众多以及图片体积过大每每会影响页面加载速度,形成不良的用户体验,因此进行图片懒加载优化势在必行。webpack
咱们先来看一下页面启动时加载的图片信息。git
如图所示,这个页面启动时加载了几十张图片(甚至更多),而这些图片请求几乎是并发的,在 Chrome 浏览器,最多支持的并发请求次数是有限的,其余的请求会推入到队列中等待或者停滞不前,直到上轮请求完成后新的请求才会发出。因此至关一部分图片资源请求是须要排队等待时间的。github
在上面能够看出,有部分图片达到几百 kB,设置 2M(这锅必须运营背,非得上传高清大图不可?),直接致使了加载时间过长。web
针对以上状况,进行图片懒加载有如下优势:vue-cli
图片懒加载的原理主要是判断当前图片是否到了可视区域这一核心逻辑实现的npm
scroll
事件,对其进行事件监听。咱们先来看下页面结构数组
<html lang="en"> <head> <meta charset="UTF-8" /> <title>Lazyload</title> <style> img { display: block; margin-bottom: 50px; height: 200px; width: 400px; } </style> </head> <body> <img src="./img/default.png" data-src="./img/1.jpg" /> <img src="./img/default.png" data-src="./img/2.jpg" /> <img src="./img/default.png" data-src="./img/3.jpg" /> <img src="./img/default.png" data-src="./img/4.jpg" /> <img src="./img/default.png" data-src="./img/5.jpg" /> <img src="./img/default.png" data-src="./img/6.jpg" /> <img src="./img/default.png" data-src="./img/7.jpg" /> <img src="./img/default.png" data-src="./img/8.jpg" /> <img src="./img/default.png" data-src="./img/9.jpg" /> <img src="./img/default.png" data-src="./img/10.jpg" /> </body> </html>
先获取全部图片的 dom,经过 document.body.clientHeight
获取可视区高度,再使用 element.getBoundingClientRect()
API 直接获得元素相对浏览的 top 值, 遍历每一个图片判断当前图片是否到了可视区范围内。代码以下:浏览器
function lazyload() { let viewHeight = document.body.clientHeight //获取可视区高度 let imgs = document.querySelectorAll('img[data-src]') imgs.forEach((item, index) => { if (item.dataset.src === '') return // 用于得到页面中某个元素的左,上,右和下分别相对浏览器视窗的位置 let rect = item.getBoundingClientRect() if (rect.bottom >= 0 && rect.top < viewHeight) { item.src = item.dataset.src item.removeAttribute('data-src') } }) }
最后给 window 绑定 onscroll
事件
window.addEventListener('scroll', lazyload)
主要就完成了一个图片懒加载的操做了。可是这样存在较大的性能问题,由于 scroll
事件会在很短的时间内触发不少次,严重影响页面性能,为了提升网页性能,咱们须要一个节流函数来控制函数的屡次触发,在一段时间内(如 200ms)只执行一次回调。
下面实现一个节流函数
function throttle(fn, delay) { let timer let prevTime return function (...args) { const currTime = Date.now() const context = this if (!prevTime) prevTime = currTime clearTimeout(timer) if (currTime - prevTime > delay) { prevTime = currTime fn.apply(context, args) clearTimeout(timer) return } timer = setTimeout(function () { prevTime = Date.now() timer = null fn.apply(context, args) }, delay) } }
而后修改一下 srcoll
事件
window.addEventListener('scroll', throttle(lazyload, 200))
经过上面例子的实现,咱们要实现懒加载都须要去监听 scroll
事件,尽管咱们能够经过函数节流的方式来阻止高频率的执行函数,可是咱们仍是须要去计算 scrollTop
,offsetHeight
等属性,有没有简单的不须要计算这些属性的方式呢,答案就是 IntersectionObserver
。
IntersectionObserver
是一个新的 API,能够自动"观察"元素是否可见,Chrome 51+ 已经支持。因为可见(visible)的本质是,目标元素与视口产生一个交叉区,因此这个 API 叫作"交叉观察器"。咱们来看一下它的用法:
var io = new IntersectionObserver(callback, option) // 开始观察 io.observe(document.getElementById('example')) // 中止观察 io.unobserve(element) // 关闭观察器 io.disconnect()
IntersectionObserver
是浏览器原生提供的构造函数,接受两个参数:callback 是可见性变化时的回调函数,option 是配置对象(该参数可选)。
目标元素的可见性变化时,就会调用观察器的回调函数 callback。callback 通常会触发两次。一次是目标元素刚刚进入视口(开始可见),另外一次是彻底离开视口(开始不可见)。
var io = new IntersectionObserver((entries) => { console.log(entries) })
callback 函数的参数(entries)
是一个数组,每一个成员都是一个 IntersectionObserverEntry
对象。举例来讲,若是同时有两个被观察的对象的可见性发生变化,entries
数组就会有两个成员。
getBoundingClientRect()
方法的返回值,若是没有根元素(即直接相对于视口滚动),则返回 nullintersectionRect
占 boundingClientRect
的比例,彻底可见时为 1,彻底不可见时小于等于 0下面咱们用 IntersectionObserver
实现图片懒加载
const imgs = document.querySelectorAll('img[data-src]') const config = { rootMargin: '0px', threshold: 0, } let observer = new IntersectionObserver((entries, self) => { entries.forEach((entry) => { if (entry.isIntersecting) { let img = entry.target let src = img.dataset.src if (src) { img.src = src img.removeAttribute('data-src') } // 解除观察 self.unobserve(entry.target) } }) }, config) imgs.forEach((image) => { observer.observe(image) })
Vue 中除了平时经常使用的 v-show
、v-bind
、v-for
等指令外,还能够自定义指令。Vue 指令定义函数提供了几个钩子函数(可选):
实现一个懒加载指令的思路
IntersectionObserver
API,若是支持就使用 IntersectionObserver
实现懒加载,不然则使用 srcoll
事件监听 + 节流的方法实现。Vue.directive
注册一个 v-lazy
的指令,暴露一个 install()
函数,供 Vue 调用。main.js
里 use(指令) 便可调用。<img>
标签的 src
换成 v-lazy
便可实现图片懒加载。代码以下
新建 LazyLoad.js
文件
const LazyLoad = { // install方法 install(Vue, options) { const defaultSrc = options.default Vue.directive('lazy', { bind(el, binding) { LazyLoad.init(el, binding.value, defaultSrc) }, inserted(el) { if (IntersectionObserver) { LazyLoad.observe(el) } else { LazyLoad.listenerScroll(el) } }, }) }, // 初始化 init(el, val, def) { el.setAttribute('data-src', val) el.setAttribute('src', def) }, // 利用IntersectionObserver监听el observe(el) { var io = new IntersectionObserver((entries) => { const realSrc = el.dataset.src if (entries[0].isIntersecting) { if (realSrc) { el.src = realSrc el.removeAttribute('data-src') } } }) io.observe(el) }, // 监听scroll事件 listenerScroll(el) { const handler = LazyLoad.throttle(LazyLoad.load, 300) LazyLoad.load(el) window.addEventListener('scroll', () => { handler(el) }) }, // 加载真实图片 load(el) { const windowHeight = document.documentElement.clientHeight const elTop = el.getBoundingClientRect().top const elBtm = el.getBoundingClientRect().bottom const realSrc = el.dataset.src if (elTop - windowHeight < 0 && elBtm > 0) { if (realSrc) { el.src = realSrc el.removeAttribute('data-src') } } }, // 节流 throttle(fn, delay) { let timer let prevTime return function (...args) { const currTime = Date.now() const context = this if (!prevTime) prevTime = currTime clearTimeout(timer) if (currTime - prevTime > delay) { prevTime = currTime fn.apply(context, args) clearTimeout(timer) return } timer = setTimeout(function () { prevTime = Date.now() timer = null fn.apply(context, args) }, delay) } }, } export default LazyLoad
在 main.js
里 use 指令
import LazyLoad from './LazyLoad.js' Vue.use(LazyLoad, { default: 'xxx.png', })
将组件内 <img>
标签的 src
换成 v-lazy
<img v-lazy="xxx.jpg" />
这样就能完成一个 vue 懒加载的指令了。
w你必须知道的webpack插件原理分析
webpack的异步加载原理及分包策略
总结18个webpack插件,总会有你想要的!
搭建一个 vue-cli4+webpack 移动端框架(开箱即用)
从零构建到优化一个相似vue-cli的脚手架
封装一个toast和dialog组件并发布到npm
从零开始构建一个webpack项目
总结几个webpack打包优化的方法
总结vue知识体系之高级应用篇
总结vue知识体系之实用技巧
总结vue知识体系之基础入门篇
总结移动端H5开发经常使用技巧(干货满满哦!)