图片懒加载
概念
其他资源加载完成再加载图片
- 已经出现在可视窗口中的图片需要进行加载
- 暂时没有出现在可视窗口中的图片应该暂时先不加载
随着页面的滚动,当图片所在盒子元素出现在可视窗口(一露头/出现一半/完全出现),再去加载图片。
原理
- 加载页面时,
img
的src
不赋值(这样就不会加载图片),把图片的地址赋值给img
的自定义属性(方便后期想要加载图片时获取) - 如果
src
不赋值,或者加载图片出错,浏览器会显示“碎图”,样式不美观,所以一开始可以隐藏img
(可以设置display
,也可以设置透明度为0(透明度变化可以设置过渡效果)) -
给图片所在的盒子设置背景占位图(或者背景颜色),在图片加载之前,用其占位。(盒子的宽高需事先设置好)
例:
.pic-box { width: 600px; height: 400px; background: #ddd; } .pic-box img { opacity: 0; transition: opacity .3s; }
什么时候加载?
- 页面第一次渲染完之后(即其他资源加载完成之后。例如:
window.onload
) - 加载出现在当前可视窗口内的图片
如何加载?
- 获取图片的前述自定义属性值,拿到图片地址
- 把地址赋给图片的
src
,如果图片可以正常加载成功,则让img
显示
传统方案
- 优势:兼容性好
- 劣势:实现麻烦
思路
页面滚动中进行监听:window.onscroll
-
设
picBox
为图片所在的元素 pic-box:const picBox = document.querySelector(selector);
-
设
A
为 pic-box 顶部到可视窗口顶部的距离:const A = picBox.getBoundingClientRect().top;
-
设
B
为 pic-box 底部到可视窗口顶部的距离:const B = pciBox.getBoundingClientRect().bottom;
-
设
C
为可视窗口的高度:const C = document.documentElement.clientHeight;
-
判断是否是时候加载
-
元素在可视窗口完全出现才加载:
B<=C && A>=0
-
元素在可视窗口还没完全出现就加载:
- 一露头就加载:
A<=C && B>0
- 出现一半时加载:类似上条,其中
A
和B
都加元素盒子一半的高度
- 一露头就加载:
-
实现
JS 部分
const imgElement = document.getElementById('img');
const imgContainer = document.getElementById('container');
/**
* 图片懒加载
* @returns {void}
*/
const lazy = () => {
if (!(imgElement instanceof HTMLImageElement)) { return; }
const realSrc = imgElement.getAttribute('real-src') || '';
// 1. 传统方法:直接对原 img 元素进行加载,无法处理图片加载失败的情况
// imgElement.src = realSrc;
// imgElement.onload = () => {
// // 图片加载成功
// imgElement.style.opacity = '1';
// };
// 2. 改进方法:确保图片地址是正确的情况下,再给页面中的IMG元素赋值,防止因图片加载失败出现“裂图”
const img = new Image();
img.src = realSrc;
img.onload = () => {
// 图片加载成功
imgElement.src = realSrc;
imgElement.style.opacity = '1';
}
// 设置自定义属性:标记当前图片已经处理过延迟加载
imgContainer.loaded = true;
};
/**
* 计算是否加载(完全出现再加载)
* @returns {void}
*/
const compute = () => {
// 如果图片已经加载过,则不再进行以下处理
if (
imgContainer.loaded
|| !(imgContainer instanceof HTMLElement)
) { return; }
// console.log('Computing'); //test: 如果不判断图片是否已经加载,则每次滚动仍都会执行以下操作
const C = document.documentElement.clientHeight;
const {
top: B,
bottom: A,
} = imgContainer?.getBoundingClientRect();
if (A <= C && B >= 0) {
lazy();
}
}
// 页面其他资源加载完后计算一次 & 页面滚动过程中随时计算
/**
* * scroll事件会在浏览器滚动条滚动过程中触发,并且按照浏览器的最快反应时间(一般5~7ms)的频率触发
* 例如:我们滚动100ms,按照5ms触发一次,一共触发20次
* 触发频率太高了,造成了没必要的计算和性能消耗
* * 此时我们需要降低触发频率(不是降低浏览器的触发频率,而是把compute函数执行的频率降下来)
* 此操作称为“函数节流”
*/
window.onload = compute;
window.onscroll = compute;
// window.onscroll = utils.throttle(compute, 300);
DOM 与样式部分
<div id="container" >
<img id="img" real-src="https://images.universal-music.de/img/assets/590/590645/4/380/00028948654734-giulini-bruckner-packshotjpg.jpg" >
</div>
<script src="./index.js" ></script>
<style>
#container {
width: 210px;
height: 90px;
position: absolute;
margin: 2000px auto;
background-color: #bbb;
}
#img {
opacity: 0;
transition: opacity 1s;
}
</style>
现代方案
- 优势:简洁方便
- 劣势:旧版浏览器不支持
思路
使用 IntersectionObserver
实现
const element1 = document.querySelector('.container1');
const element2 = document.querySelector('.container2');
const element3 = document.querySelector('.container3');
// 创建监听器
const observer = new IntersectionObserver(changes => {
/**
* 回调函数执行:
* * 创建监听器、且监听了DOM元素会立即执行一次
* (连续监听多个DOM只触发一次,但是如果监听是分隔开的,每新监听一个元素都会触发执行一次)
* * 当监听的元素和可视窗口交叉状态改变,也会触发执行
* (默认是“一露头”或者“完全出现”,会触发;
* 也可以基于第二个Options配置项中的threshold来指定规则)
* * threshold: [0] // 一露头 & 完全出现
* * ...
* * threshold: [1] // 完全出现 & 出现一点
* ----
* changes: 是一个数组,记录了每一个监听元素和可视窗口的交叉信息
* * boundingClientRect: 记录当前监听元素的getBoundingClientRect获取的值
* * isIntersecting: true / false,true代表出现在可视窗口中,false则反之
* * target: 存储当前监听的这个DOM元素对象
* * ...
*/
const [item] = changes;
console.log({item});
}, { threshold: [0, 0.5, 1] });
// 监听某个DOM元素和可视窗口的交叉状态改变;unobserve移除监听
observer.observe(element1);
observer.observe(element2); //连续监听多个DOM只触发一次回调
setTimeout(() => {
observer.observe(element3); //如果监听是分隔开的,每新监听一个元素都会触发执行一次回调
}, 1000);
实现
const imgContainer = document.querySelector('.container');
const imgElement = imgContainer.querySelector('img');
const lazy = () => { // 延迟加载
const realSrc = imgElement.getAttribute('real-src');
imgElement.src = realSrc;
imgElement.onload = () => {
imgElement.style.opacity = '1';
};
};
// 创建监听器监听图片元素的容器,控制延迟加载:无需再进行复杂运算、无需考虑函数节流……
const observer = new IntersectionObserver(([item]) => {
if (!item.isIntersecting) { return; }
// 完全出现在视口中时:进行延迟加载
lazy();
// 处理过的需要移除监听
observer.unobserve(item.target);
}, { threshold: [1] });
observer.observe(imgContainer);
电商平台商品图放大镜效果
光标处于 mark 中间,鼠标移动时,控制 mark 在 abbre 中移动
注意点:
- mark 移动的距离不能超过 abbre
- 同时需要控制 detailImg 在 detail 中移动
长宽比例关系:
mark / abbre = detail / detailImg
可知 detailImg = detail / (mark / abbre)
位置关系:
-
mark 的位置 = 光标相对于页面的位置 - abbre 的偏移 - mark 宽高的一半
因为光标在盒子中间
-
mark 也不可以超过 abbre 的边界:
- 最小
top
、left
= 0 - 最大
top
、left
= abbre - mark
- 最小
拖动排序
const data = [
{ index: 0, hash: 'yule', name: '娱乐', background: 'red' },
{ index: 1, hash: 'yinyue', name: '音乐', background: 'lightsalmon' },
{ index: 2, hash: 'wudao', name: '舞蹈', background: 'lightseagreen' },
{ index: 3, hash: 'shenghuo', name: '生活', background: 'blue' },
];
let zIndex = 0;
const init = data => {
const contentElement = document.getElementById('content');
const tabBox = document.getElementById('tabBox');
let contentStr = '';
let tabStr = '';
data.forEach(i => {
contentStr += `<div id="${i.hash}" style="background: ${i.background}">
${i.name}</div>`;
tabStr += `<li><a href="#${i.hash}">${i.name}</a></li>`;
});
contentElement.innerHTML = contentStr;
console.log({tabStr})
tabBox.innerHTML = tabStr;
const liList = tabBox.getElementsByTagName('li');
const move = function (e) {
// 阻止拖动的默认行为
e.preventDefault();
// 跟随光标
this.style.top = e.pageY - this.y + this.top + 'px';
// 动画效果
for (let i = 0; i < liList.length; i++) {
// 如果这个liList[i]就是this,就不用比较了
if (liList[i] === this) { continue; }
// 当前项
const currentLi = liList[i];
console.log(this.offsetTop, currentLi.offsetTop, this.i, currentLi.i);
if (this.offsetTop > currentLi.offsetTop
&& this.i < currentLi.i
) {
/* 如果当前项的 offsetTop 大于了 currentLi 的 offsetTop,
就让 currentLi 把自己的位置让出来 */
currentLi.style.marginTop = -currentLi.offsetHeight + 'px';
}
if (this.offsetTop < currentLi.offsetTop
&& this.i < currentLi.i
) { //还原
currentLi.style.marginTop = 0 + 'px';
}
if (this.offsetTop < currentLi.offsetTop
&& this.i > currentLi.i
) { //下面的目录项往上移时的变化
currentLi.style.marginTop = currentLi.offsetHeight + 'px';
}
if (this.offsetTop > currentLi.offsetTop
&& this.i > currentLi.i
) { //还原
currentLi.style.marginTop = 0 + 'px';
}
console.log(currentLi.style.marginTop);
}
}
// 倒着是因为有样式问题,因为定位会脱离文档流
for (let i = liList.length - 1; i >= 0; i--) {
// console.log(liList[i].offsetTop, liList[i].offsetLeft, liList[i].offsetParent)
liList[i].style.top = liList[i].offsetTop + 'px';
liList[i].style.position = 'absolute';
liList[i].i = data[i].index;
liList[i].onmousedown = function (e) {
this.y = e.pageY;
this.top = Number.parseFloat(this.style.top);
// 永远保持被拖动的盒子在最上层
this.style.zIndex = ++zIndex;
// 用事件委托绑定鼠标移动事件
document.onmousemove = move.bind(this);
}
window.ondragstart = e => { //须阻止页面触发 drag 相关事件,否则后面的 mouseup 不触发
e.preventDefault();
e.stopPropagation();
};
liList[i].onmouseup = function () {
// 清除document上的移动事件
document.onmousemove = null;
for (let i = 0; i < liList.length; i++) {
// 要把当前的 li 和被拖动的 li 的距离存起来
liList[i].distance = Math.abs(this.offsetTop - liList[i].offsetTop);
}
// 根据distance排序
let ary = [...liList].sort((a, b) => a.distance - b.distance);
console.log(ary.map(i => i.style.marginTop));
// 找出 ary 里 marginTop 不是 0px 的 li
let close = ary.find(i => i.style.marginTop //排除掉自己(未设置 marginTop 故为空)
&& i.style.marginTop !== '0px'
);
if (!close) { //若顺序没有改变,则复位
return this.style.top = this.top + 'px';
}
// 取到当前被拖动的 li
let current = data.splice(this.i, 1)[0];
data.splice(close.i, 0, current);
data.forEach((item, index) => {
item.index = index
})
init(data);
}
}
};
init(data);
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="content" class="content"></div>
<div>
<ul id="tabBox" class="tab-box"></ul>
</div>
</body>
<script src="./index.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
ul,
li {
list-style: none;
}
a {
display: block;
text-decoration: none;
color: #000;
}
#content {
overflow: hidden;
background: pink;
margin: 0 auto;
width: 800px;
}
#content div {
height: 200px;
font-size: 40px;
line-height: 200px;
text-align: center;
}
#tabBox {
position: relative;
width: 50px;
position: fixed;
left: 0;
top: 50px;
/* top: 0px; */
}
#tabBox a {
text-align: center;
float: left;
width: 100px;
border: 1px solid #000;
height: 50px;
font-size: 20px;
line-height: 50px;
}
#tabBox li {
float: left;
user-select: none;
transition: margin .2s ease 0s;
display: block;
background: #fff;
margin-top: 0;
}
</style>
</html>
//未完待xu