从零实现瀑布流--图片懒加载及其底层原理

文章不短(我写的很详细) 耐心看完 给你一个极好性能的瀑布流及图片懒加载

瀑布流效果
瀑布流
何为瀑布流

瀑布流,又称瀑布流式。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。最早采用此布局的网站是Pinterest,逐渐在国内流行开来。国内大多数清新站基本为这类风格。

即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。

那么规则是什么呢?
下面通过图解来分析一下瀑布流的算法。

图解瀑布流算法

当第一排排满足够多的等宽图片时(如下图情况),自然而然的考虑到之后放置的图片会往下面排放。

图一

那么第六张图片,放置在什么位置呢?是下图的位置么?

图二

答案是:不是,那我们应该怎样排列呢?
为了减小每列的差距我们应该将后面六张图片中最高的那一个放到当前这六列中最矮的那一列,正数第二高的图片放到倒数第二矮的那一列......以此类推

图三
  • 好了,实现的思想大家已经看了,接下来咱们用代码实现一个三列的瀑布流
实现代码
  1. 先看HTML、CSS
  • HTML
<body>
  <div class="container clearfix">
    <div class="column">
      <!-- <div class="card">
                <a href="#">
                    <div class="lazyImageBox">
                        <img src="" alt="" data-image="images/1.jpg">
                    </div>
                    <p>泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证</p>
                </a>
            </div> -->
    </div>
    <div class="column"></div>
    <div class="column"></div>
  </div>
</body>
  • CSS
html,
body {
    background: #D6D7DB;
}

.container {
    box-sizing: border-box;
    margin: 20px auto;
    width: 760px;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
}

.container .column {
    box-sizing: border-box;
    width: 240px;
}

.card {
    margin-bottom: 10px;
    padding: 5px;
    background: #FFF;
    box-shadow: 3px 3px 10px 0 #222;
}

.card a {
    display: block;
}

.card a .lazyImageBox {
    overflow: hidden;
}

.card a .lazyImageBox img {
    width: 100%;
}

.card a p {
    margin-top: 5px;
    color: #000;
    font-size: 12px;
    line-height: 20px;
}
  • 创造了一个宽度为760px的容器 使用css3弹性盒使内容两端对齐然后将内容分为3列,每列240 像素
  1. 获取数据
  • 模拟数据(创建data.json)
<data.json>
[
  {
    "id": 1,
    "pic": "images/9.jpg",
    "width": 300,
    "height": 433,
    "title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
    "link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
  },
  {
    "id": 2,
    "pic": "images/5.jpg",
    "width": 300,
    "height": 200,
    "title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
    "link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
  },
  {
    "id": 3,
    "pic": "images/3.jpg",
    "width": 300,
    "height": 170,
    "title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
    "link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
  },
  {
    "id": 4,
    "pic": "images/2.jpg",
    "width": 300,
    "height": 300,
    "title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
    "link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
  },
  {
    "id": 5,
    "pic": "images/3.jpg",
    "width": 300,
    "height": 170,
    "title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
    "link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
  },
  {
    "id": 6,
    "pic": "images/10.jpg",
    "width": 300,
    "height": 257,
    "title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
    "link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
  },
  {
    "id": 7,
    "pic": "images/6.jpg",
    "width": 300,
    "height": 400,
    "title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
    "link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
  },
  {
    "id": 8,
    "pic": "images/5.jpg",
    "width": 300,
    "height": 200,
    "title": "泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证",
    "link": "https://baike.sogou.com/v1850208.htm?fromTitle=%E9%9C%89%E9%9C%89"
  }
]
  • 创建utils.js
  • 简单封装ajax
<utils.js>
class utils {
  constructor() {
    console.log('工具类');
  }
  /**
   * 封装Ajax
   * @param {*} url 
   * @returns {Promise}
   */
  ajax (url) {
    return new Promise(resolve => {
      let xhr = new XMLHttpRequest;
      xhr.open('get', url);
      xhr.onreadystatechange = function () {
        (xhr.readyState === 4 && xhr.status === 200) && resolve(JSON.parse(xhr.responseText));
      };
      xhr.send();
    });
  }
}
  • 使用ajax获取数据并渲染页面 注意看注释
  let data = await utils.ajax('./data.json');
  bindHTML(data);
  // 获取到所有class为column的元素并将类数组转化为数组实例
  let columns = Array.from(document.querySelectorAll('.column'));
  /**
   * 数据绑定
   * @param {Array} data 
   */
  function bindHTML (data) {
    // 根据服务器返回的图片的宽高,动态计算出图片放在230容器中,高度应该怎么缩放
    // 因为我们后期要做图片的延迟加载,在没有图片之前,我们也需要知道未来图片要渲染的高度,这样才能又一个容器先占位

   // 根据容器宽度(230)等比缩放数据中的图片
   data.forEach(item => {
     let { width, height } = item;
     item.height = height / (width / 230);
     item.width = 230;
   })

    // 每三个为一组获取数据
    for (let i = 0; i < data.length; i += 3) {
      // 将数组分为3列一组
      let group = data.slice(i, i + 3);

      // 获取到当前每一列的高度并进行升序处理
      columns.sort((a, b) => b.offsetHeight - a.offsetHeight);

      // 把每一组的数据按照图片高度进行降序处理
      group.sort((a, b) => a.height - b.height);

      // 把告诉最小的图片插入到高度最大的列中 (看不懂继续看图解)
      group.forEach((item, index) => {
        let { height, title, pic } = item;
        // 创建存放图片的容器
        let card = document.createElement('div');
        card.className = "card";
        // 使用字符模板(es6)为容器添加内容
        card.innerHTML = `<a href="#">
                    <div class="lazyImageBox" style="height:${height}px">
                        <img src="${pic}" alt="">
                    </div>
                    <p>${title}</p>
                </a>`;
        // 向每一列中添加数据
        columns[index].appendChild(card);
      });
    }
  }
  • 至此我们实现了瀑布流---接下来我们将实现图片的懒加载
/*
 * 为啥要做图片的延迟加载
 *   浏览器渲染页面
 *      1.构建DOM树
 *      2.构建CSSOM树
 *      3.生成RENDER TREE (渲染树)
 *      4.布局
 *      5.分层
 *      6.珊格化
 *      7.绘制
 * 构建DOM树中如果遇到img
 *   老版本浏览器:阻碍DOM渲染
 *   新版本浏览器:不会阻碍  每一个图片请求都会占用一个HTTP(浏览器同时发送的HTTP 6个)
 *          拿回来资源后会和RENDER TREE一起渲染
 *   .....
 *   开始加载图片,一定会让页面第一次渲染速度变慢(白屏)
 *
 * 图片延迟加载:第一次不请求也不渲染图片,等页面加载完,其他资源都渲染好了,再去请求加载图片
 */
懒加载的优点
  1. 增强用户体验
  2. 优化代码
  3. 减少http的请求
  4. 减少服务器端压力
  5. 服务器的按需加载
懒加载的原理

先将img标签中的src链接设为同一张图片(空白图片),将其真正的图片地址存储再img标签的自定义属性中(比如data-src)。当js监听到该图片元素进入可视窗口时,即将自定义属性中的地址存储到src属性中,达到懒加载的效果。
这样做能防止页面一次性向服务器响应大量请求导致服务器响应慢,页面卡顿或崩溃等问题。

  • 接下来我将使用 getBoundingClientRect方法实现图片大的懒加载
    getBoundingClientRect 方法兼容性较好 但不是唯一的解决方案 在文章最后我会简单介绍几种其他的几种方案
  1. 首先我们来了解下 getBoundingClientRect
  • 理解:getBoundingClientRect用于获取某个元素相对于视窗的位置集合。集合中有top, right, bottom, left等属性。

  • 返回值类型

  1. top:元素上边到视窗顶部的距离;
  2. right:元素右边到视窗顶部的距离;
  3. bottom:元素下边到视窗顶部的距离;
  4. left:元素左边到视窗顶部的距离;
  • 图解


    getBoundingClientRect
  • 接下来我们使用代码实现图片的懒加载 注意看注释

// 创建一个对象存放需要加载的图片列表
let lazyImageBoxs;
// 获取浏览器可视区域高度
let winH = document.documentElement.clientHeight;

function lazyFunc () {
  !lazyImageBoxs ? lazyImageBoxs = Array.from(document.querySelectorAll('.lazyImageBox')) : null;
   lazyImageBoxs.forEach(lazyImageBox => {
    // 已经处理过则不在处理  详情请看 lazyImg 函数
    let isLoad = lazyImageBox.getAttribute('isLoad');
    if (isLoad) return;
    // 若图片底部距离浏览器顶部的距离小于浏览器的可视区域高度  那么说明此图片已完全显示出来
    let { bottom } = lazyImageBox.getBoundingClientRect();
    bottom <= winH && lazyImg(lazyImageBox);
  });
}

/**
 * 懒加载图片
 * @param {dom} lazyImageBox 
 */
function lazyImg (lazyImageBox) {
  // img:获取到当前元素中的图片,trueImg:需要渲染的图片路径
  let img = lazyImageBox.querySelector('img'),
    trueImg = img.getAttribute('data-image');
  // 加载图片
  img.src = trueImg;
  // 移除自定义属性
  img.removeAttribute('data-image');
  // 添加isLoad属性:表示当前图片已经处理过了
  lazyImageBox.setAttribute('isLoad', 'true');
}

// 当页面渲染完成或滚动时都执行图片懒加载函数
window.onload = lazyFunc;
window.onscroll = lazyFunc;
  • 至此我们就实现了图片的懒加载
    问题:注意看以下两行代码
window.onload = lazyFunc;
window.onscroll = lazyFunc;

这样的弊端是什么

onscroll触发的频率太高了,滚动一下可能要被触发很多次,导致很多没必要的计算和处理,消耗性能

  • 演示
    我们在以下函数中 添加console.log('OK'); 看页面滚动的时候会触发多少次
function lazyImg (lazyImageBox) {
  console.log('OK');
  //......
}

接下来我们看控制台的输出


这是我滑动滚轮5次函数执行的次数,看到这个数字你是否为页面的性能感到焦虑,那么应该如何处理这种问题呢?
这就用到了 函数节流
接下来我们在utils 工具类中写一个节流方法

class utils {
  constructor() {
    console.log('工具类');
  }
  /*
     * throttle:实现函数的节流(目的是频繁触发中缩减频率)
     *   @params
     *      func:需要执行的函数
     *      wait:自己设定的间隔时间(频率)
     *   @return
     *      可被调用执行的函数
     */
  throttle (func, wait = 500) {
    let timer = null,
      previous = 0; //记录上一次操作时间
    return function anonymous (...params) {
      let now = new Date(), //当前操作的时间
        remaining = wait - (now - previous);
      if (remaining <= 0) {
        // 两次间隔时间超过频率:把方法执行即可
        clearTimeout(timer);
        timer = null;
        previous = now;
        func.call(this, ...params);
      } else if (!timer) {
        // 两次间隔时间没有超过频率,说明还没有达到触发标准呢,设置定时器等待即可(还差多久等多久)
        timer = setTimeout(() => {
          clearTimeout(timer);
          timer = null;
          previous = new Date();
          func.call(this, ...params);
        }, remaining);
      }
    };
  }
  /**
   * 封装Ajax
   * @param {*} url 
   * @returns {Promise}
   */
  ajax (url) {
    return new Promise(resolve => {
      let xhr = new XMLHttpRequest;
      xhr.open('get', url);
      xhr.onreadystatechange = function () {
        (xhr.readyState === 4 && xhr.status === 200) && resolve(JSON.parse(xhr.responseText));
      };
      xhr.send();
    });
  }
}
  • 最后来so easy啦
window.onload = lazyFunc;
window.onscroll = utils.throttle(lazyFunc, 500);
  • 让我们看下节流后的效果


    节流后效果

    我们可以看到同样是滚动5次 函数的执行次数由149 次变为11次

其他懒加载方式:

  1. new IntersectionObserver
  2. 直接为img 添加loading="lazy"属性:目前只有Chrome64 版本已上才兼容 感觉这将是未来的趋势

具体实现方法我就不一一写出来了,大家有兴趣的可以了解下,这两种方法比现在用的这种方法要好美中不足的是兼容性还不行

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343