原理
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样子对于页面加载性能上会有很大的提升,也提高了用户体验。
将页面中的img标签src指向一张小图片或者src为空,然后定义data-src属性指向真实的图片。src指向一张默认的图片,否则当src为空时也会向服务器发送一次请求。可以指向loading图的地址。
当载入页面时,先把可视区域内的img标签的data-src属性值赋给src,然后监听滚动事件,把用户即将看到的图片加载。这样便实现了懒加载。
实现
实现比较简单,直接上代码
var num = document.getElementsByTagName('img').length;
var img = document.getElementsByTagName("img");
//存储图片加载到的位置,避免每次都从第一张图片开始遍历
var n = 0;
//页面载入完毕加载可视区域内的图片
lazyLoad();
window.onscroll = lazyload;
function lazyLoad(){
//可见区域高度
var seeHeight = window.innerHeight || document.documentElementHeight || document.body.clientHeight;
//滚动条距离顶部高度
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
for(var i = n; i < num; i++){
if(img[i].offsetTop < scrollTop + seeHeight){
if (img[i].getAttribute("src") == "default.png") {
img[i].src = img[i].getAttribute("data-src");
}
n = i + 1;
}
}
}
这里的判断也可以这样写
//元素上边距离页面上边的距离
var imgTop = img[i].getBoundingClientRect().top;
if(imgTop < seeHeight){
//lazyload...
}
使用节流函数进行性能优化
如果直接将函数绑定在scroll事件上,当页面滚动时,函数会被高频触发,这非常影响浏览器的性能。
节流函数:只允许一个函数在N秒内执行一次。下面是一个简单的节流函数
var COUNT = 0;
function testFn() { console.log(COUNT++); }
// 浏览器resize的时候
// 1. 清除之前的计时器
// 2. 添加一个计时器让真正的函数testFn延后100毫秒触发
// 等不再resize的时候过100ms才触发
window.onresize = function(){
var timer = null;
clearTimeout(timer);
timer = setTimeout(function(){
testFn();
}, 100);
};
细心的同学会发现上面的代码其实是错误的:setTimeout 函数返回值应该保存在一个相对全局变量里面,否则每次 resize 的时候都会产生一个新的计时器(在任务队列中,每次独立执行),这样就达不到我们发的效果了
var timer = null;
window.onresize = function(){
clearTimeout(timer);
timer = setTimeout(function(){
testFn();
}, 100);
};
这时候代码就正常了,但是又多了一个新问题 —— 产生了一个全局变量 timer。这是我们不想见到的,如果这个页面还有别的功能也叫 timer 不同的代码之前就是产生冲突。为了解决这个问题我们要用js的一个语言特性:闭包
var throttle = function (fn, delay) {
var timer = null;
return function () {
clearTimeout(timer);
timer = setTimeout(function() {
fn();
}, delay);
}
};
window.onresize = throttle(testFn, 200);
但是现在又有一个问题:如果用户不断的 resize 浏览器窗口大小,这时延迟处理函数一次都不会执行。于是我们又要添加一个功能:当用户触发 resize 的时候应该 在某段时间 内至少触发一次,既然是在某段时间内,那么这个判断条件就可以取当前的时间毫秒数,每次函数调用把当前的时间和上一次调用时间相减,然后判断差值如果大于 某段时间 就直接触发,否则还是走 timeout 的延迟逻辑。
var throttle = function (fn, delay, atleast) {
var timer = null;
var previous = null;
return function () {
var now = +new Date();
if (!previous) previous = now;
if (now - previous > atleast) {
fn();
// 重置上一次开始时间为本次结束时间
previous = now;
} else {
clearTimeout(timer);
timer = setTimeout(function() {
fn();
}, delay);
}
}
};
window.onresize = throttle(testFn, 200, 1000);
最后我们来实现一下我们的lazyLoad函数
function throttle(fn, delay, time) {
var timeout,
previous = new Date();
return function() {
var context = this,
args = arguments,
now = new Date();
clearTimeout(timeout);
// 如果达到了规定的触发时间间隔,触发 handler
if (now - previous >= time) {
//采用函数内部的this(非严格模式下是window),避免this被DOM元素绑定
fn.apply(context, args);
previous = now;
// 没达到触发间隔,重新设定定时器
} else {
timeout = setTimeout(fn, delay);
}
};
};
function lazyload(event){
//...
}
// 采用了节流函数
window.addEventListener('scroll', throttle(lazyload, 500, 1000));
判断元素是否在当前视野中
直接上代码
const isInViewport = ()=> {
const refRect = defaultScreenRect = defaultScreenRect || {
top: 0,
bottom: window.innerHeight,
left: 0,
right: window.innerWidth
}
const rect = el.getBoundingClientRect()
//获得元素的四面尺寸
const diff = {
left: Math.max(rect.left, refRect.left),
//right为元素右边界或者视窗右边界,当元素右边界超过视窗右边界为屏幕右边界
right: Math.min(rect.right, refRect.right),
top: Math.max(rect.top, refRect.top),
bottom: Math.min(rect.bottom, refRect.bottom)
}
//前一个参数代表左右边界是否有一个在视窗内
//后一个参数代表上下边界是否有一个在视窗内
const area = (diff.right - diff.left) * (diff.bottom - diff.top)
return area > 0
}
getBoundingClientRect()方法返回元素的大小及其相对于视口的位置。
对于文档加载状态的判断
直接上代码
const throttle = (fn, immediate) => {
//如果立即执行一次,immediate为true
if (immediate) {
switch (document.readyState) {
case 'loading':
document.addEventListener('readystate', () => { // 在页面 interactive/complete 之后再进行加载
document.readyState !== 'loading' && setTimeout(fn, 0)
})
break
case 'interactive':
case 'complete':
default: // 网上说Android 2.3.5机器上是loaded, https://stackoverflow.com/questions/13346746/document-readystate-on-domcontentloaded
setTimeout(fn, 0)
}
}
let timer = null
return () => {
//高频触发的时候在这里截断
timer && clearTimeout(timer);
timer = setTimeout(fn, 100) // 人眼能识别的连续帧是1秒25帧,200ms(10帧)用户会感觉到变化
}
}
export function addScrollListener(callback = () => {}, immediate) {
callback = throttle(callback, immediate)
// 监听并主动执行一次
document.addEventListener(
'scroll',
callback,
//这个变量用来检测浏览器是否支持passive,需要单独一个模块来检测
checkPassiveSupported ? {passive: true} : false
)
return callback
}
document.readyState属性描述了文档的加载状态。loading仍在加载。
interactive表示文档已经完成加载,文档已被解析,但是诸如图像,样式表和框架之类的子资源仍在加载。
complete表示文档和所有子资源已完成加载。状态表示 load 事件即将被触发。
当这个属性的值变化时,document 对象上的readystatechange 事件将被触发。