原生JS实现最简单的图片懒加载

144

原文地址:原生JS实现最简单的图片懒加载

欢迎star。

如果有错误的地方欢迎指正。


Demo地址:http://axuebin.com/lazyload

照片都是自己拍的哦~

懒加载

什么是懒加载

懒加载其实就是延迟加载,是一种对网页性能优化的方式,比如当访问一个页面的时候,优先显示可视区域的图片而不一次性加载所有图片,当需要显示的时候再发送图片请求,避免打开网页时加载过多资源。

什么时候用懒加载

当页面中需要一次性载入很多图片的时候,往往都是需要用懒加载的。

懒加载原理

我们都知道HTML中的<img>标签是代表文档中的一个图像。。说了个废话。。

<img>标签有一个属性是src,用来表示图像的URL,当这个属性的值不为空时,浏览器就会根据这个值发送请求。如果没有src属性,就不会发送请求。

嗯?貌似这点可以利用一下?

我先不设置src,需要的时候再设置?

nice,就是这样。

我们先不给<img>设置src,把图片真正的URL放在另一个属性data-src中,在需要的时候也就是图片进入可视区域的之前,将URL取出放到src中。

实现

HTML结构

<div class="container">
  <div class="img-area">
    <img class="my-photo" alt="loading" data-src="./img/img1.png">
  </div>
  <div class="img-area">
    <img class="my-photo" alt="loading" data-src="./img/img2.png">
  </div>
  <div class="img-area">
    <img class="my-photo" alt="loading" data-src="./img/img3.png">
  </div>
  <div class="img-area">
    <img class="my-photo" alt="loading" data-src="./img/img4.png">
  </div>
  <div class="img-area">
    <img class="my-photo" alt="loading" data-src="./img/img5.png">
  </div>
</div>

仔细观察一下,<img>标签此时是没有src属性的,只有altdata-src属性。

alt 属性是一个必需的属性,它规定在图像无法显示时的替代文本。
data-* 全局属性:构成一类名称为自定义数据属性的属性,可以通过HTMLElement.dataset来访问。

如何判断元素是否在可视区域

方法一

网上看到好多这种方法,稍微记录一下。

  1. 通过document.documentElement.clientHeight获取屏幕可视窗口高度
  2. 通过element.offsetTop获取元素相对于文档顶部的距离
  3. 通过document.documentElement.scrollTop获取浏览器窗口顶部与文档顶部之间的距离,也就是滚动条滚动的距离

然后判断②-③<①是否成立,如果成立,元素就在可视区域内。

方法二(推荐)

通过getBoundingClientRect()方法来获取元素的大小以及位置,MDN上是这样描述的:

The Element.getBoundingClientRect() method returns the size of an element and its position relative to the viewport.

这个方法返回一个名为ClientRectDOMRect对象,包含了toprightbottonleftwidthheight这些值。

MDN上有这样一张图:

可以看出返回的元素位置是相对于左上角而言的,而不是边距。

我们思考一下,什么情况下图片进入可视区域。

假设const bound = el.getBoundingClientRect();来表示图片到可视区域顶部距离;
并设 const clientHeight = window.innerHeight;来表示可视区域的高度。

随着滚动条的向下滚动,bound.top会越来越小,也就是图片到可视区域顶部的距离越来越小,当bound.top===clientHeight时,图片的上沿应该是位于可视区域下沿的位置的临界点,再滚动一点点,图片就会进入可视区域。

也就是说,在bound.top<=clientHeight时,图片是在可视区域内的。

我们这样判断:

function isInSight(el) {
  const bound = el.getBoundingClientRect();
  const clientHeight = window.innerHeight;
  //如果只考虑向下滚动加载
  //const clientWidth = window.innerWeight;
  return bound.top <= clientHeight + 100;
}

这里有个+100是为了提前加载。

经提醒。。这个方法性能

加载图片

页面打开时需要对所有图片进行检查,是否在可视区域内,如果是就加载。

function checkImgs() {
  const imgs = document.querySelectorAll('.my-photo');
  Array.from(imgs).forEach(el => {
    if (isInSight(el)) {
      loadImg(el);
    }
  })
}

function loadImg(el) {
  if (!el.src) {
    const source = el.dataset.src;
    el.src = source;
  }
}

这里应该是有一个优化的地方,设一个标识符标识已经加载图片的index,当滚动条滚动时就不需要遍历所有的图片,只需要遍历未加载的图片即可。

函数节流

在类似于滚动条滚动等频繁的DOM操作时,总会提到“函数节流、函数去抖”。

所谓的函数节流,也就是让一个函数不要执行的太频繁,减少一些过快的调用来节流。

基本步骤:

  1. 获取第一次触发事件的时间戳
  2. 获取第二次触发事件的时间戳
  3. 时间差如果大于某个阈值就执行事件,然后重置第一个时间
function throttle(fn, mustRun = 500) {
  const timer = null;
  let previous = null;
  return function() {
    const now = new Date();
    const context = this;
    const args = arguments;
    if (!previous){
      previous = now;
    }
    const remaining = now - previous;
    if (mustRun && remaining >= mustRun) {
      fn.apply(context, args);
      previous = now;
    }
  }
}

这里的mustRun就是调用函数的时间间隔,无论多么频繁的调用fn,只有remaining>=mustRunfn才能被执行。

实验

页面打开时

可以看出此时仅仅是加载了img1和img2,其它的img都没发送请求,看看此时的浏览器

第一张图片是完整的呈现了,第二张图片刚进入可视区域,后面的就看不到了~

页面滚动时

当我向下滚动,此时浏览器是这样

此时第二张图片完全显示了,而第三张图片显示了一点点,这时候我们看看请求情况

img3的请求发出来,而后面的请求还是没发出~

全部载入时

当滚动条滚到最底下时,全部请求都应该是发出的,如图

完整demo

在这哦:http://axuebin.com/lazyload

原文地址:http://axuebin.com/blog/2017/...

更新

方法三 IntersectionObserver

经大佬提醒,发现了这个方法

先附上链接:

jjc大大:https://github.com/justjavac/the-front-end-knowledge-you-may-dont-know/issues/10

阮一峰大大:http://www.ruanyifeng.com/blog/2016/11/intersectionobserver_api.html

API Sketch for Intersection Observers:https://github.com/WICG/IntersectionObserver

IntersectionObserver可以自动观察元素是否在视口内。

var io = new IntersectionObserver(callback, option);
// 开始观察
io.observe(document.getElementById('example'));
// 停止观察
io.unobserve(element);
// 关闭观察器
io.disconnect();

callback的参数是一个数组,每个数组都是一个IntersectionObserverEntry对象,包括以下属性:

属性 描述
time 可见性发生变化的时间,单位为毫秒
rootBounds 与getBoundingClientRect()方法的返回值一样
boundingClientRect 目标元素的矩形区域的信息
intersectionRect 目标元素与视口(或根元素)的交叉区域的信息
intersectionRatio 目标元素的可见比例,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0
target 被观察的目标元素,是一个 DOM 节点对象

我们需要用到intersectionRatio来判断是否在可视区域内,当intersectionRatio > 0 && intersectionRatio <= 1即在可视区域内。

代码

function checkImgs() {
  const imgs = Array.from(document.querySelectorAll(".my-photo"));
  imgs.forEach(item => io.observe(item));
}

function loadImg(el) {
  if (!el.src) {
    const source = el.dataset.src;
    el.src = source;
  }
}

const io = new IntersectionObserver(ioes => {
  ioes.forEach(ioe => {
    const el = ioe.target;
    const intersectionRatio = ioe.intersectionRatio;
    if (intersectionRatio > 0 && intersectionRatio <= 1) {
      loadImg(el);
    }
    el.onload = el.onerror = () => io.unobserve(el);
  });
});

你可能感兴趣的

55 条评论
hope_pdm · 2017年08月24日

答主,有时间写写window下部署jekyll啊。基本要疯

+7 回复

elten · 2017年08月20日

适合流量不大的站。另外,搜索引擎拉取的网站是找不到图片的,对于seo非常不友好。

+5 回复

2

@elten 那请问这应该如何利于seo呢?

blff122620 · 2017年08月21日
0

以后的seo抓取,肯定会支持img的src没有,就用data-src作为 图片路径。
另外,seo和单页程序(用户体验)都是相矛盾的,呵呵。

michaelzhouh · 2017年09月08日
wind_stone · 2017年08月22日

element.offset 返回当前元素相对于其 offsetParent 元素的顶部的距离,不是相对于文档顶部的距离呀

+5 回复

某熊猫桑 · 2017年08月19日

+4 回复

0

受教了 。。我好好看看。。

axuebin 作者 · 2017年08月19日
jkol · 2017年08月21日

判断元素是否在可视区方法一后面的判断式如果是②-③<①,那么刚开始状态②为0, ③大于等于0, ①大于等于0
, 则②-③小于等于0,则图片在打开页面的时候全都会被加载,是不是应该改为③-②<①

+4 回复

3

+1

PrConstantin · 2017年08月21日
6

我。。我还又测了一遍。。我能说我写反了么。。下次一定检查完再发。。

axuebin 作者 · 2017年08月21日
1

谢谢。。

axuebin 作者 · 2017年08月21日
jllllyuz · 2017年08月21日

为什么给出的DEMO没有效果呢,什么也没加载,只有LOADING几个字

+4 回复

武汉大魔王 · 2017年08月19日

防抖动那个地方用setTimeout会不会更好 极端情况下500ms内就滚动了那不就无法触发懒加载了?

+3 回复

2

现在这个是无论怎么样,500ms后都会执行啊。你说的是设一个定时器,然后不停滚动就是刷新定时器?

axuebin 作者 · 2017年08月19日
1

对啊,在测试的时候如果快速滚动,停下后,停下位置的图片不会触发加载。

FreeLoop · 2017年08月20日
0

@FreeLoop 我发现了!谢谢!我瞅瞅是为什么

axuebin 作者 · 2017年08月20日
々流火々 · 2017年10月13日

大佬遇到一个问题 项目打包好后 all.min.js中 main.js 也就是控制懒加载的那段js得很后面才能生效 在教差网络环境下 我等待js 开始执行到懒加载这段js就得4秒 而图片加载出来也就这个时间 首屏加载太捉急 有什么好的解决方案么 老大不让 把这段js单独分离出来

+3 回复

武汉大魔王 · 2017年08月19日

主要是在想setTimeout能不能起到相同的作用

+2 回复

jokerchan · 2017年08月19日

const timer = null;是做什么的呢?
之前有设置过定时器的想法么

+2 回复

1

23333......

axuebin 作者 · 2017年08月19日
1

我第一次发评论啊!不会炸了吧!!

jokerchan · 2017年08月19日
1

@jokerchan 我是之前看的函数节流和防抖,然后测好就丢那了... 那时候有试过用setTimeout来实现节流,但是没写出来... 可能那时候忘了删。。。

axuebin 作者 · 2017年08月19日
Xxxdxs · 2017年08月20日

+2 回复

menduo · 2017年08月21日

棒!

+1 回复

1

@menduo 谢谢

axuebin 作者 · 2017年08月21日
Hoyt · 2017年08月21日

display none 可以阻止图片加载为什么不试试?

+1 回复

1

@Hoyt 应该是也加载的吧

axuebin 作者 · 2017年08月21日
0

@axuebin 你试试,img标签父级div加 displaynone。浏览器是不会加载的。

Hoyt · 2017年08月21日
0

@Hoyt 页面上虽然没显示,但是请求是已经发出去了啊

axuebin 作者 · 2017年08月21日
HowardTangHw · 2017年08月21日

方法2中

if (!previous){
  previous = now;
}
const remaining = now - previous;
if (mustRun && remaining >= mustRun) 

那第一次是不是就会不执行了呢?

+1 回复

0

好像是这样。。这就是为什么上面提到的,快速拉动滚动条,并没有加载的原因....

axuebin 作者 · 2017年08月21日
1

@axuebin 我看了下上面大家给的链接~http://www.cnblogs.com/fsjohn... 感觉定义的时候直接给个0就好啦

HowardTangHw · 2017年08月21日
旅行的意义zxy · 2017年08月21日

第三个方法的兼容性怎么样呀

+1 回复

2

看看我附上的链接哈

axuebin 作者 · 2017年08月21日
0

@axuebin 刚刚用caniuse看了,兼容Edge,Firefox,chrome,opera和Android IE和IOS和OperaMini不兼容

旅行的意义zxy · 2017年08月21日
武汉大魔王 · 2017年08月20日

我去 居然讨论到现在 不过楼主那个节流器是没有问题的 毕竟是500ms

回复

1

不不不 问题是有的,像上面那个兄弟说的,快速滚动之后不会发送请求

axuebin 作者 · 2017年08月20日
1

@axuebin 对于快速滚动有什么好的解决办法么?需要考虑到用户的恶意操作行为

Miss_Ye · 2017年08月22日
Corwien · 2017年08月20日

用插件会不会方便点

回复

3

插件是另外一回事... 就是看看这个东西怎么实现啊

axuebin 作者 · 2017年08月20日
isLishude · 2017年08月21日

第三个方法也是第一次知道

回复

1

@isLishude 我也是大佬提醒了才知道……

axuebin 作者 · 2017年08月21日
你瞒_我瞒 · 2018年03月11日

请问,这个方法有兼容问题吗

回复

jiabing520a · 2018年06月08日

iOS 上都不支持貌似,图片加载不出来,有办法解决吗?

回复

载入中...