前言
懒加载时一种也页面效果,可以提高页面加载速率,并降低服务器的贷款和资源损耗。
一、图片实现懒加载步骤
- 先清空图片的 src 属性值,以使浏览器不加载图片
- 然后再在需要加载图片时,为图片添加上 src 属性
二、案例练习(三国女将)
1. html 代码
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>三国女将</title>
<link rel="stylesheet" href="./css/三国女将.css">
<script type="text/javascript" src="./js/transformCSS.js"></script>
<script type="text/javascript" src="./js/tweenAnimation.js"></script>
<script type="text/javascript" src="./js/touchscroll.js"></script>
</head>
<body>
<div id="app">
<header>三国女将</header>
<main id="main">
<div id="content">
<ul id="imgList">
</ul>
<div class="pull-up-update">上拉加载更多……</div>
</div>
</main>
<div id="big-image-page">
<header>大图预览
<span class="close">×</span>
</header>
<section id="show-area">
<img id="big-image" src="" alt="">
</section>
</div>
</div>
<script type="text/javascript" src="./js/三国女将.js"></script>
</body>
</html>
2. css 代码
/*清除默认样式*/
* {
padding: 0;
margin: 0;
}
ul {
list-style: noen;
}
/*全局样式控制*/
html,
body {
width: 100%;
height: 100%;
}
#app {
width: 100%;
height: 100%;
overflow: hidden;
}
/*设置样式*/
/*头部*/
header {
position: relative;
z-index: 10;
height: 10vh;
background-color: #000000;
color: white;
font-family: 苹方;
font-size: 20px;
text-align: center;
line-height: 10vh;
}
header .close {
background-color: #00CC00;
position: absolute;
right: 10px;
top: 25px;
display: block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
font-size: 28px;
}
/*主体*/
main {
height: 90vh;
}
main #content {
position: relative;
}
main #content ul {
overflow: hidden;
}
main #content ul li {
float: left;
width: 46vw;
height: 46vw;
margin: 2vw;
border-radius: 4vw;
overflow: hidden;
background-image: url("../img/loadingImg.gif");
background-repeat: no-repeat;
background-position: center center;
}
main #content ul li img {
width: 100%;
height: 100%;
}
main #content .pull-up-update {
position: absolute;
width: 100%;
height: 20vh;
bottom: -20vh;
font-size: 24px;
line-height: 20vh;
text-align: center;
background-color: #acd3e3;
}
#big-image-page {
transform: scale(0);
z-index: 10;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: white;
}
#big-image-page #show-area {
position: relative;
width: 100%;
height: 90vh;
}
#big-image-page #show-area img {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
}
/*# sourceMappingURL=三国女将.css.map */
3. js 代码
- 三国女将
(function(){
/*阻止浏览器默认行为 , 如果通过 documentElement 阻止默认行为需要关闭被动模式*/
var app = document.getElementById("app");
app.addEventListener("touchstart",function(e){
e.preventDefault();
});
//获取元素
var main = document.getElementById("main");
var content = main.querySelector("#content");
var imgList = content.querySelector("#imgList");
var pullUpUpdate = content.querySelector(".pull-up-update");
var bigImagePage =document.getElementById("big-image-page");
var close = bigImagePage.querySelector("header .close");
var bigImage = bigImagePage.querySelector("#show-area #big-image");
//每次 / 每页呈现的图片数量
var num = 16;
//当前显示的页码
var page =1;
//滑动状态标识
var isMoving = false;
/*
* 初始化图片数据仓库
* 100 张图片
* */
var imgData = ["./img/1.jpg"];
for(var i=0;i<100;i++){
// imgData[i]='https://picsum.photos/id/'+i+'/400/300';
imgData[i]="./img/"+(i%18+1)+".jpg";
}
//声明函数创建 li 标签 , 根据页码呈现图片
function creatLi(){
/*
* 第 1 页 0 16
* 2 16 32
* 3 32 48
* N (N-1)*16 N*16
* */
var start = (page-1)*num;
var end = page * num;
for(var i=start;i<end;i++){
var li = document.createElement("li");
//为 li 添加自定义属性 data-src , 用于保存当前 li 里的 img 的图片路径
li.dataset.src = imgData[i];
/*//为 li 添加自定义属性 data-isloaded , 标识当前 li 为未加载状态
li.dataset.isloaded = 0;*/
//为 li 添加自定义属性 loaded ,用于标识当前 li 为未加载状态
li.setAttribute("loaded",0);
var img = document.createElement("img");
li.appendChild(img);
imgList.appendChild(li);
}
//页码自增
page++;
//图片懒加载
lazyload();
}
creatLi();
/*主体滚动*/
var touchscroll = new Touchscroll('main','#content',{
width:4,
//移动时的回调函数
move:function(){
/*
* 问题: 惯性移动时不加载图片
* 解决: 在 tweenAnimation 的定时器中增加回调函数参数用来加载图片
* */
//惯性移动时也加载图片
lazyload();
/*
* 上拉时底部元素缩放
* 超出临界点时 , 缩放比例 = (content当前的 translateY 值 - content 的最小 translateY 值) / 底部元素的高度
* */
//获取当前的 translateY
var translateY = transformCSS(content,'translateY');
//计算最小的 translateY
var minTranslateY = main.offsetHeight - content.offsetHeight;
//判断是否到达临界点
if(translateY < minTranslateY){
//计算底部元素的缩放比例
pullUpUpdate.scale = Math.min(Math.abs(translateY-minTranslateY)/pullUpUpdate.offsetHeight,1);
//已经滑出边界 , 设置底部元素的显示比例
transformCSS(pullUpUpdate,'scale',pullUpUpdate.scale);
}
},
//结束时的回调函数
end:function(){
/*
* 上拉加载更多图片
* 底部元素全部出来时松手加载更多图片
* */
//判断底部元素是否显示完全
if(pullUpUpdate.scale >= 1){
creatLi();
//更新滚动条高度
touchscroll.init();
//获取滚动条
var scrollBar = document.querySelector(".scroll-bar");
//关闭定时器
if(scrollBar.timer && scrollBar.timer['translateY']){
clearInterval(scrollBar.timer['translateY']);
}
if(content.timer && content.timer['translateY']){
clearInterval(content.timer['translateY']);
}
//修改滚动条位置
var scrollBarTranslateY = -main.offsetHeight * transformCSS(content,'translateY') / content.offsetHeight;
transformCSS(scrollBar,'translateY',scrollBarTranslateY);
/*
* 滚动条位置问题 滑动状态问题
* 原因: 在 touchscroll.js 中 , 修改滚动条位置调用了 tweenAnimation.js 中的定时器
* 在定时器中会执行 move 回调函数
* 由于定时器的延时 , 在 end 回调函数执行完毕 , 但定时器的回调函数还在执行
* 导致定时器将滚动条位置改回错误的 traslateY , 也会将滑动状态标识 改回 true
* 滚动条问题解决: 先关闭定时器再修改滚动条位置 , 为保持内容与滚动条位置一致 , 也需要关闭内容的定时器
* 滑动状态问题解决: 单独为 main 绑定事件
* */
}
//重设底部元素的缩放比例为 0
pullUpUpdate.scale = 0;
}
});
/*
* 绑定touchmove 事件 , 检测图片是否进入可视区
* 进入可视区后判断图片是否已经加载
* 如果图片未加载 , 则加载图片
* */
main.addEventListener("touchmove",function(){
//屏幕滑动时加载图片
lazyload();
//修改滑动状态标识为 true
isMoving = true;
});
main.addEventListener("touchend",function(){
//修改滑动状态标识为 false
isMoving = false;
})
/*
* 大图显示
* 问题: 新增元素没有事件
* 解决: 事件委托 或重新为新创建元素绑定事件
* */
/*//获取所有 li
var lis = document.querySelectorAll("#imgList li");
lis.forEach(function(li){
li.addEventListener("touchend",function(e){
//判断滑动状态 , 如果当前正在滑动 , 直接 return 返回
if(isMoving) return;
//设置大图的 src
bigImage.src = this.dataset.src;
//修改大图页面的 scale 为 1
transformCSS(bigImagePage,'scale',1);
//为大图页面添加过渡
bigImagePage.style.transition = 'transform 0.5s';
//获取触点位置
var x = e.changedTouches[0].clientX;
var y = e.changedTouches[0].clientY;
//设置过渡的起始位置 transform-origin
bigImagePage.style.transformOrigin = x+'px '+y+'px';
});
});*/
//绑定事件委托
imgList.addEventListener("touchend",function(e){
//判断事件源 nodeName === 'IMG'
if(e.target.nodeName === 'IMG'){
//执行事件响应函数
//判断滑动状态 , 如果当前正在滑动 , 直接 return 返回
if(isMoving) return;
//设置大图的 src
bigImage.src = e.target.src;
//修改大图页面的 scale 为 1
transformCSS(bigImagePage,'scale',1);
//为大图页面添加过渡
bigImagePage.style.transition = 'transform 0.5s';
//获取触点位置
var x = e.changedTouches[0].clientX;
var y = e.changedTouches[0].clientY;
//设置过渡的起始位置 transform-origin
bigImagePage.style.transformOrigin = x+'px '+y+'px';
}
});
close.addEventListener("touchstart",function(e){
//修改大图页面的 scale 为 0
transformCSS(bigImagePage,'scale',0);
});
//创建函数实现图片懒加载功能
function lazyload(){
//获取所有 li
var lis = document.querySelectorAll("#imgList li");
lis.forEach(function(li){
/*
* 图片懒加载
* 检测图片位置 , 滚动高度 + 视口高度 == 图片相对于父元素的 offsetTop 时 , 说明已经到达临界状态
* */
//获取当前 li 到定位父元素的偏移量
var oT = li.offsetTop;
//main 的高度
var h = main.offsetHeight;
//content 的滚动高度
var translateY = -transformCSS(content,'translateY');
//判断 li 是否进入可视区
if(oT <= h+translateY){
//判断当前 li 是否已经加载 , 如果已经加载就直接 return 返回
/*if(li.dataset.isloaded == 1) return;*/
if(li.getAttribute("loaded") == 1) return;
//说明 li 已经进入可视区 , 此时才加载图片
//获取当前 li 里面的 img
var img = li.querySelector("img");
/*浮入效果*/
img.style.opacity = 0;
img.style.transition = "opacity 0.8s";
//延时加载图片
setTimeout(function(){
//设置 img 的 src 属性 , 等于当前 li 的 data-src 属性值
img.src = li.dataset.src;
//图片加载完毕后修改透明度
img.onload = function(){
this.style.opacity = 1;
};
//图片加载失败 , 图片数据仓库中没有该图片
img.onerror = function(){
this.style.opacity = 1;
console.error("图片数据仓库中没有该图片!");
this.src = './img/noimage.png';
}
},1000);
//修改加载状态为已加载
/*li.dataset.isloaded = 1;*/
li.setAttribute("loaded",1);
}
});
}
})();
- transformCSS
//将 transformCSS 封装为全局函数
(function(w){
function transformCSS(ele,prop,val){
/*
* 设置: transformCSS(ele,'translateX',100)
* 读取: transformCSS(ele,'translateX') 只能读取通过本方法设置的 transform 样式
* 参数: ele 元素对象
* prop 变形样式 字符串
* val 样式值
* */
//判断当前对象是否已有样式仓库
if(ele.store === undefined){
//没有样式仓库 , 创建初始化样式仓库对象,用于保存样式
ele.store={};
}
//设置样式 , arguments 中封装了实参
if(arguments.length == 3){
//将参数保存到样式仓库中
ele.store[prop] = val;
//创建局部变量用于表示 transform 样式值
var str = '';
//遍历仓库对象的属性
for(var i in ele.store){
//根据属性保存样式值
switch (i) {
//平移
case 'translateX':
case 'translateY':
case 'translateZ':
str += i + '(' + ele.store[i] + 'px) ';
break;
//旋转
case 'rotate':
case 'rotateX':
case 'rotateY':
case 'rotateZ':
str += i + '(' + ele.store[i] + 'deg) ';
break;
//缩放
case 'scale':
case 'scaleX':
case 'scaleY':
case 'scaleZ':
str += i + '(' + ele.store[i] + ') ';
break;
}
}
//为元素设置样式
ele.style.transform = str;
}
//读取样式
if(arguments.length == 2){
//判断当前对象的样式仓库中是否含有该样式
if(ele.store[prop]){
//含有该样式 , 则返回样式仓库中的样式
//注意: 只能读取通过本方法设置的 transform 样式
return ele.store[prop];
}
//当前样式仓库中没有该样式 , 返回默认值
/*
* 根据样式名返回不同值:
* translate / rotate 0
* scale 1
* */
//判断是否以 scale 开头
var start = prop.substr(0,5);
if(start === 'scale'){
//是 scale
return 1;
}else{
//是 rotate 或translate
return 0;
}
}
}
w.transformCSS=transformCSS;
})(window);
//匿名函数自调用 , 用以暴露闭包中的局部变量
- tweenAnimation
/*
* 功能: 实现元素动画过渡 backEaseout easeOut Linear
* 函数名: tweenAnimation
* 参数: ele 元素对象
* style 样式 字符串
* init 起始状态
* end 结束状态
* time 过渡时间
* jiange 动画间隔时间
* type 动画类型 字符串
* callback 回调函数 , 在定时器中执行
* 使用示例: tweenAnimation(ele,'width',200,1000,5000,10,'backEaseOut')
*
* function backEaseOut(t,b,c,d,s){
if (s == undefined) s = 1.70158;
return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
}
* function easeOut(t,b,c,d){
return -c * ((t=t/d-1)*t*t*t - 1) + b;
}
* function Linear(t,b,c,d){ return c*t/d + b; }
*
* 如: 2s 内元素的 top 值从 200 过渡到 9000 , 每次动画时间间隔为 10 , easeOut
* tweenAnimation(ele,'top',200,9000,2000,10,'easeOut')
* 依赖: transformCSS.js
* */
function tweenAnimation(ele,style,init,end,time,jiange,type,callback){
//定义 tween 对象 , 保存计算过渡的函数
var tween = {
backEaseOut: function (t,b,c,d,s){
if (s == undefined) s = 1.70158;
return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
},
easeOut: function (t,b,c,d){
return -c * ((t=t/d-1)*t*t*t - 1) + b;
},
Linear: function (t,b,c,d){
return c*t/d + b;
}
}
//type 参数初始化 , 默认为 Linear
var type = type === undefined ? 'Linear' : type;
// tween 函数参数初始化 t b c d
var t = 0; //开始时间
var b = init; //起始样式
var c = end-init; //样式变化量
var d = time; //动画过渡总时间
/*
* 定时器冲突问题:
* setIntervar() 函数的返回结果是一个作为当前定时器标识的数字
* 当多次调用定时器时 , 会覆盖之前创建的定时器的标识
* 导致前面的定时器无法被清除
* 解决: 将所有定时器标识保存到对象的定时器样式样式仓库中 , 通过样式名来标识当前定时器
* */
//为对象添加 timer 属性 , 是一个对象 , 表示定时器样式仓库 , 用于保存元素的所有定时器标识
if(ele.timer === undefined){
//如果当前元素没有定时器样式仓库 , 则添加样式仓库属性 timer 并初始化
ele.timer = {};
}
//设置定时器 , 通过样式标识对应定时器
ele.timer[style] = setInterval(function(){
//4. 当时间 t 达到结束时间 time 时,关闭定时器
/*
* 注意: 第四步应该写到最前面
* 应该在 t 自增之前判断是否已经到达结束时间 , 然后根据判断结果决定是否执行后面的代码
* */
if(t >= d){
clearInterval(ele.timer[style]);
return;
}
//1. 时间自增
t += jiange;
//2. 每一个间隔时间后 , 调用 tween 对象中的 [type]方法 , 计算出时间 t 时的新的样式值
var v = tween[type](t,b,c,d);
//3. 为元素对象设置新的样式
switch (style){
//判断样式并设置
case 'width':
case 'height':
case 'left':
case 'top':
ele.style[style] = v +'px';
break;
case 'translateX':
case 'translateY':
case 'translateZ':
case 'scale':
case 'scaleX':
case 'scaleY':
case 'scaleZ':
case 'rotate':
case 'rotateX':
case 'rotateY':
transformCSS(ele,style,v);
break;
case 'opcity':
ele.style[style] = v;
}
//执行回调函数
if(callback && typeof callback === 'function'){
callback();
}
},jiange);
}
- touchscroll
/*
* 构造函数: Touchscroll
* 功能: 实现竖向触摸滑动
* 参数: container //包裹元素 选择器字符串
* content //滚动元素 选择器字符串
* options{
* backgroundColor //滚动条的背景颜色 , 默认为 rgba(0,0,0,0.6)
* width //滚动条的宽度 , 默认为 6px
* move //函数 , 在滑动时执行
* }
* 使用示例: new Touchscroll("#container",".wrapper",{
* width: 4,
* backgroundColor: 'rgb(228,228,228)'
* });
* <div id="container">
* <div class="wrapper"></div>
* </div>
* 缺点: 需要手动设置包裹元素的 position: relative height overflow: hidden
* 依赖: transformCSS.js
* tweenAnimation.js
*
* */
function Touchscroll(container,content,options){
//获取元素
var app = document.querySelector(container);
var main = app.querySelector(content);
//move 时的回调函数
var moveCallback = options && options.move ? options.move : null;
/*创建滚动条*/
var scrollBar = document.createElement("div");
//将滚动条添加到容器中
app.appendChild(scrollBar);
/*
* 滚动条初始化
* 为滚动条添加 scroll-bar 类
* 设置滚动条样式
* .scroll-bar{
position: absolute;
right: 0;
top: 0;
width: 6px;
border-radius: 3px;
background-color: rgba(0,0,0,0.6);
}
* 将滚动条添加到 包裹元素 中
* */
this.init = function(){
//添加 scroll-bar 类
scrollBar.className = "scroll-bar";
//设置滚动条样式
//定位
scrollBar.style.position = "absolute";
scrollBar.style.right = 0;
scrollBar.style.top = 0;
//宽度
var scrollBarWidth = options && options.width ? options.width : 6;
scrollBar.style.width = scrollBarWidth +'px';
//圆角
scrollBar.style.borderRadius = "3px";
//背景颜色
var bg = options && options.backgroundColor ? options.backgroundColor : "rgba(0,0,0,0.6)";
scrollBar.style.backgroundColor = bg;
//设置滚动条的高度
//计算滚动条应该设置的高度 滚动条的高度 / app高度 = app高度 / content高度
var h = (app.offsetHeight * app.offsetHeight) / main.offsetHeight;
scrollBar.style.height = h +'px';
//给外层包裹元素增加 相对定位
app.style.position = 'relative';
};
this.init();
app.init = this.init;
//文档加载完毕 , 初始化滚动条
window.addEventListener('load', function(){
app.init();
});
app.addEventListener("touchstart",function(e){
//获取触摸开始时的时间
this.touchstartTime = Date.now();
//获取开始时的触点位置
this.touchY = e.changedTouches[0].clientY;
//获取触摸开始时 content 的 translateY 的值
this.contentT = transformCSS(main,'translateY');
//即点即停
if(main.timer && main.timer['translateY']){
clearInterval(main.timer['translateY']);
}
//导航条即点即停
if(scrollBar.timer && scrollBar.timer['translateY']){
clearInterval(scrollBar.timer['translateY']);
}
});
app.addEventListener("touchmove",function(e){
//取消过渡效果
main.style.transition = "none";
//获取移动时的触点位置
this.touchY2 = e.changedTouches[0].clientY;
//计算 content 应该设置的 translateY 的值
var t = this.contentT + (this.touchY2 - this.touchY);
/*橡皮筋效果*/
//定义缩减系数
var K = 0.5;
//边界控制
if(t >= 0 || t <= app.offsetHeight - main.offsetHeight){
t = this.contentT + (this.touchY2 - this.touchY) * K;
}
//设置 content 的 translateY 的值
transformCSS(main,'translateY',t);
/*
* 滚动条移动
* 滚动条移动距离 / app 的高度 = content移动距离 / content的高度
* 注意应该要对 content 移动距离取反
* */
//计算 scrollBar 应该移动的距离
var y = -app.offsetHeight * t / main.offsetHeight;
//设置 scrollBar 的 translateY 的值
transformCSS(scrollBar,'translateY',y);
//执行 move 回调函数
if(options && typeof options.move === 'function'){
options.move();
}
});
app.addEventListener("touchend",function(e){
//获取触摸结束时的时间
this.touchendTime = Date.now();
//获取结束时的触点位置
this.touchY3 = e.changedTouches[0].clientY;
//计算 content 没有惯性移动时应该设置的 translateY 的值
var t = this.contentT + (this.touchY3 - this.touchY);
/*惯性滑动*/
//定义时间系数
var T = 100;
//获取滑动速度
var v = (this.touchY3 - this.touchY)/(this.touchendTime - this.touchstartTime);
//计算惯性移动的距离
var s = v * T;
//新的 translateY 值
t += s;
/*导航条跟随惯性移动*/
//计算加上惯性移动滚动条移动的距离
var y = -app.offsetHeight * t / main.offsetHeight;
/*回弹效果*/
// 声明变量 , 用于保存变形效果 , 默认为 easeOut
var type = 'easeOut';
if(t >= 0){
t = 0;
type = 'backEaseOut';
}else if(t <= app.offsetHeight - main.offsetHeight){
t = app.offsetHeight - main.offsetHeight;
type = 'backEaseOut';
}
/*导航条跟随回弹*/
if(y <= 0){
y = 0;
type = 'backEaseOut';
}else if(y >= app.offsetHeight - scrollBar.offsetHeight ){
y = app.offsetHeight - scrollBar.offsetHeight;
type = 'backEaseOut';
}
/*
* 即点即停效果
* 直接使用 transition 的缺点: 无法即点即停,惯性移动时,触摸元素并滑动会取消过渡效果导致元素直接跳到最终位置
* 可以使用 tweenAnimation() 函数执行过渡 , 然后在 touchstart 事件中清除定时器以实现即点即停效果
* 注意:
* 一定要及时清除定时器 , 不然每触发一次 touchend 事件就会开启一个定时器
* */
//获取 content 当前的 translateY 值
var currentContentY = transformCSS(main,'translateY');
//移动 content
tweenAnimation(main,'translateY',currentContentY,t,500,10,type,moveCallback);
//获取 scrollBar 当前的 translateY 值
var currentScrollY = transformCSS(scrollBar,'translateY');
//移动 scrollBar
tweenAnimation(scrollBar,'translateY',currentScrollY,y,500,10,type);
//执行 end 回调函数
if(options && typeof options.end === 'function'){
options.end();
}
});
}
执行结果
(完)