瀑布流布局

  • 内容框宽度固定,高度不固定。
  • 内容框从左到右排列,一行排满后,其余内容框就会按顺序排在短的一列后。

实现效果

image.png

实现思路

  1. 编写结构样式 三列 每一列中都有很多项 每一项的高度由图片的真实高度决定
  2. 发送ajax请求 从服务器获取数据
  3. 按照瀑布流的规则实现动态绑定 (每次拿出三条数据,把最小的图片插入到最高的列中)
  4. 为了优化页面的第一次渲染速度,我们最开始,并不会去加载真实的图片,当内容渲染完后,我们再加载真实的图片(图片的懒加载) 只有完全出现在可视窗口的图片 再去加载真实的图片
  • 通过获取图片底部距离body父级参照物的上偏移(A) 和 获取页面底部距离父级参照物的偏移(B) A<=B 表示出现在了视口中
  • 基于getBoundingClientRect优化懒加载条件
  • IntersectionObserver ES6 新的监听 能够监听一个或者多个盒子和浏览器可视窗口的交叉信息
  1. 当页面滚动到底部,我们去加载更多的数据

HTML

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>瀑布流</title>
  8. <!-- IMPORT CSS -->
  9. <link rel="stylesheet" href="css/reset.min.css">
  10. <link rel="stylesheet" href="css/index.css">
  11. </head>
  12. <body>
  13. <div class="container" id="fallsBox">
  14. <!-- 第一列 -->
  15. <div class="column">
  16. <!-- <div class="item">
  17. <a href="">
  18. <div class="pic-box">
  19. <img src="./images/10.jpg" alt="">
  20. </div>
  21. <p class="desc-box">
  22. 泰勒·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾州,美国歌手、演员。2006年出道,同年发行专辑《泰勒·斯威夫特》,该专辑获得美国唱片业协会的白金唱片认证
  23. </p>
  24. </a>
  25. </div> -->
  26. </div>
  27. <!-- 第二列 -->
  28. <div class="column">
  29. </div>
  30. <!--第三列 -->
  31. <div class="column">
  32. </div>
  33. </div>
  34. <div class="loadMoreBox"></div>
  35. <!-- IMPORT JS -->
  36. <script src="js/index.js"></script>
  37. </body>
  38. </html>

CSS

编写css不成文小规定 1.控制显示和隐藏 2.控制位置 3.width/border/padding

html,body{
    min-height: 100%;
    background-color: rgb(219, 219, 219);
    overflow-x: hidden;
}
.container{
    box-sizing: border-box;
    width: 760px;
    margin: 20px auto;

    /* 布局 */
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
}


.container .column{
   width: 240px;
}

.container .column .item{
    margin-bottom: 10px;
    padding: 5px;
    background:#fff;
    box-shadow: 3px 3px 10px 0 #222;
}

.container .column .item a{
    display: block;
}

.container .column .item .pic-box{
    background: #e6e5e5;
    /* height: 50px; 动态添加高度*/ 
}
.container .column .item  .pic-box img{
    display: block;
    width: 100%;
    height: 100%; /*高度和宽度依靠父级盒子*/

    opacity: 0; /*默认不显示*/
    transition: opacity .3s ease;
}
/* .container .column .item  .pic-box img[src=""] {
    display: none;
} */
.container .column .item .dec-box{
    margin-top: 10px;
    color:black;
    line-height: 20px;
}

/*更多加载的盒子*/
.loadMoreBox{
    height: 50px;
    background: transparent;
    /* background-color: red; */
}

JS

①方案1 window.onscroll+用到了函数节流

  • window.onscroll+用到了函数节流
    • ONSCROLL事件 会在浏览器最快反应事件内触发一次 (谷歌:5~7MS IE10~17MS) 这样频率太快了
    • 我们需要降低它的频率,【方案:函数的节流】

图片的懒加载思路

图片懒加载1.png

加载更多思路

滚动到底部加载更多数据.png


let fallsModel = (function () {

    // 获取元素
    let fallsBox = document.querySelector('#fallsBox'),
        columns = Array.from(document.querySelectorAll('.column')),
        imgList = [];

    let data = [];

    // 工具类方法:offset 获取当前元素距离BODY的偏移(top/left)

    const offset = function offset(element) {
        let parent = element.offsetparent,
            top = element.offsetTop,
            left = element.offsetLeft;

        while (parent) {
            top += parent.clientTop + parent.offsetTop;
            left += parent.clientLeft + parent.offsetLeft;
            parent = parent.offsetparent;
        }
        return {
            top,
            left
        }
    };


    // 工具类方法:函数节流处理 
    const throttle = function throttle(func, wait) {
        if (typeof func !== "function") throw new TypeError('func must be an function');
        if (typeof wait !== "number") wait = 300;
        let timer,
            previous = 0;
        return function proxy(...params) {
            let now = +new Date(),
                remaining = wait - (now - previous),
                self = this,
                result;
            if (remaining <= 0) {
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                }
                result = func.call(self, ...params);
                previous = now;
            } else if (!timer) {
                timer = setTimeout(() => {
                    if (timer) {
                        clearTimeout(timer);
                        timer = null;
                    }
                    result = func.call(self, ...params);
                    previous = +new Date();
                }, remaining);
            }
            return result;
        };
    };




    // 1.从服务器获取数据==========================================================
    const queryData = function queryData() {
        let xhr = new XMLHttpRequest();
        xhr.open("GET", "./data.json", false);
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4 && xhr.status === 200) {
                data = JSON.parse(xhr.responseText);
            };
        };
        xhr.send();
    };

    // 2.动态渲染数据=====================================================================================
    //瀑布流规则 每一次从数据中取出3条数据(group) 三列(colums)
    //三条数据,按照图片高度排序[小,中,大]三列
    //盒子,按照高度进行排序[大,中,小]

    const binding = function binding() {
        // 服务器获取到的图片宽度高度,按照真实渲染的宽度(230)动态计算出真实渲染的高度
        // 服务器获取到的宽高 width:300 height:433   300/433 宽高比 
        // 真实渲染的宽度为width:230px 求真实渲染的高度? 高度为 230/300/433

        // 用数组中的map方法
        data = data.map((item) => {
            let { width, height } = item;
            item.width = 230;
            item.height = 230 / (width / height);
            return item;//在把更改完的元素返回
        });

        let i = 0, //如果不想生成块级上下文 则写在外面 不想产生闭包
            group;
        for (; i < data.length; i += 3) {
            group = data.slice(i, i + 3); //从当前索引 取3项
            //i=0  0 1 2 
            //i=3  3 4 5
            group.sort((a, b) => {//按照图片高度 从小到大排序
                return a.height - b.height;
            });

            columns.sort((a, b) => {//按照盒子的高度 从大到小排序
                return b.offsetHeight - a.offsetHeight;
            });

            // 把排好序的数据依次插入到对应的排好序的列中
            group.forEach((item, index) => {
                let { pic, height, title, link } = item;
                // 动态创建每一个图片盒子 插入到指定列
                let box = document.createElement("div");
                box.className = 'item';
                box.innerHTML = `<a href="${link}" target="_blank">
                <div class="pic-box" style="height:${height}px;">
                    <img src="" alt="" data-img="${pic}">
                </div>
                <p class="desc-box">
                    ${title}
                </p>
            </a>`;
                // 插入到指定列
                columns[index].appendChild(box);
            });
        };

        // 数据绑定完成后,获取到所有的IMG
        imgList = Array.from(fallsBox.querySelectorAll('.pic-box img'));



    };


    // 3.图片的延迟加载============================================================
    //  +单张图片
    const singleImgLazy = function singleImgLazy(img) {
        // 传递的Img就是需要加载的图片对象
        let pic = img.getAttribute('data-img'),
            temp = new Image();//=>document.createElement('img')
        temp.src = pic;
        temp.onload = () => {
            //加载成功:图片地址是正确的
            img.src = pic;
            img.style.opacity = 1; //更改透明度
        };
        // 处理过的IMG设置已经处理过的标识
        img.setAttribute('isLoad', 'TRUE');
    };

    // 循环图片集合 进行懒加载
    const lazyImgsLoading = function lazyImgsLoading() {
        //ONSCROLL事件 会在浏览器最快反应事件内触发一次 (谷歌:5~7MS IE10~17MS) 这样频率太快了
        //  +我们需要降低它的频率,【方案:函数的节流】
        imgList.forEach(item => {
            // 已经处理过的IMG就无需判断了
            if (item.getAttribute('isLoad')) return;
                // item 当前迭代图片
                let imgBox = item.parentNode, //父元素盒子
                    HTML = document.documentElement;
            A = imgBox.offsetHeight + offset(imgBox).top;//图片盒子距离Body的上偏移
            B = HTML.clientHeight + HTML.scrollTop;//窗口视口距离Body的上偏移
            if (A <= B) {
                singleImgLazy(item); //加载图片
            };
        });
    };

    //4.加载更多数据======================
    let count=0;
    const loadMoreData=function loadMoreData(){
        let HTML=document.documentElement;
        // HTML.scrollTop+HTML.clientHeight+100 =>卷去的高度+可视窗口高度+100(误差)
        // HTML.scrollHeight页面真实的高度约等于
        if(HTML.scrollTop+HTML.clientHeight+100>=HTML.scrollHeight){
            // 滚动到底部了
            count++;
            if(count>5)return; //模拟加载5次,数据加载完不在进行处理
            queryData();
            binding();
            lazyImgsLoading();
        }
    };
    return {
        init() {
            queryData();//获取数据
            binding();//动态绑定数据 按照瀑布流规则
            lazyImgsLoading();//延迟加载
            window.onscroll =throttle(()=>{
                lazyImgsLoading();
                loadMoreData();
            },500); //滚动的时候 500MS
        }
    };
})();

fallsModel.init();

②方案2 基于getBoundingClientRect 优化懒加载条件

image.png

// 基于getBoundingClientRect 优化懒加载条件==============================
let fallsModel = (function () {
    // 获取元素
    let fallsBox = document.querySelector('#fallsBox'),
        columns = Array.from(document.querySelectorAll('.column')),
        imgList = [];

    let data = [];

    // 工具类方法:函数节流处理 
    const throttle = function throttle(func, wait) {
        if (typeof func !== "function") throw new TypeError('func must be an function');
        if (typeof wait !== "number") wait = 300;
        let timer,
            previous = 0;
        return function proxy(...params) {
            let now = +new Date(),
                remaining = wait - (now - previous),
                self = this,
                result;
            if (remaining <= 0) {
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                }
                result = func.call(self, ...params);
                previous = now;
            } else if (!timer) {
                timer = setTimeout(() => {
                    if (timer) {
                        clearTimeout(timer);
                        timer = null;
                    }
                    result = func.call(self, ...params);
                    previous = +new Date();
                }, remaining);
            }
            return result;
        };
    };




    // 1.从服务器获取数据==========================================================
    const queryData = function queryData() {
        let xhr = new XMLHttpRequest();
        xhr.open('GET', './data.json', false);
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4 && xhr.status === 200) {
                data = JSON.parse(xhr.responseText);
            };
        };
        xhr.send();
    };

    // 2.动态渲染数据=====================================================================================
    //瀑布流规则 每一次从数据中取出3条数据(group) 三列(colums)
    //三条数据,按照图片高度排序[小,中,大]三列
    //盒子,按照高度进行排序[大,中,小]

    const binding = function binding() {
        // 服务器获取到的图片宽度高度,按照真实渲染的宽度(230)动态计算出真实渲染的高度
        // 服务器获取到的宽高 width:300 height:433   300/433 宽高比 
        // 真实渲染的宽度为width:230px 求真实渲染的高度? 高度为 230/300/433

        // 用数组中的map方法
        data = data.map((item) => {
            let { width, height } = item;
            item.width = 230;
            item.height = 230 / (width / height);
            return item;//在把更改完的元素返回
        });

        let i = 0, //如果不想生成块级上下文 则写在外面 不想产生闭包
            group;
        for (; i < data.length; i += 3) {
            group = data.slice(i, i + 3); //从当前索引 取3项
            //i=0  0 1 2 
            //i=3  3 4 5
            group.sort((a, b) => {//按照图片高度 从小到大排序
                return a.height - b.height;
            });

            columns.sort((a, b) => {//按照盒子的高度 从大到小排序
                return b.offsetHeight - a.offsetHeight;
            });

            // 把排好序的数据依次插入到对应的排好序的列中
            group.forEach((item, index) => {
                let { pic, height, title, link } = item;
                // 动态创建每一个图片盒子 插入到指定列
                let box = document.createElement('div');
                box.className = 'item';
                box.innerHTML = `<a href="${link}" target="_blank">
                <div class="pic-box" style="height:${height}px;">
                    <img src="" alt="" data-img="${pic}">
                </div>
                <p class="desc-box">
                    ${title}
                </p>
            </a>`;
                // 插入到指定列
                columns[index].appendChild(box);
            });
        };

        // 数据绑定完成后,获取到所有的IMG
        imgList = Array.from(fallsBox.querySelectorAll('.pic-box img'));
    };


    // 3.图片的延迟加载============================================================
    //  +单张图片
    const singleImgLazy = function singleImgLazy(img) {
        // 传递的Img就是需要加载的图片对象
        let pic = img.getAttribute('data-img'),
            temp = new Image();//=>document.createElement('img')
        temp.src = pic;
        temp.onload = () => {
            //加载成功:图片地址是正确的
            img.src = pic;
            img.style.opacity = 1; //更改透明度
        };
        // 处理过的IMG设置已经处理过的标识
        img.setAttribute('isLoad', 'TRUE');
    };

    // 循环图片集合
    const lazyImgsLoading = function lazyImgsLoading() {
        //ONSCROLL事件 会在浏览器最快反应事件内触发一次 (谷歌:5~7MS IE10~17MS) 这样频率太快了
        //  +我们需要降低它的频率,【方案:函数的节流】
        imgList.forEach(item => {
            // 已经处理过的IMG就无需判断了
            if (item.getAttribute('isLoad')) return;
            // item 当前迭代图片
            let imgBox = item.parentNode, //父元素盒子
                HTML = document.documentElement,
                // 基于getBoundingClientRect 优化懒加载条件
                A = imgBox.getBoundingClientRect().bottom,
                B = HTML.clientHeight;
            if (A <= B) {
                singleImgLazy(item); //加载图片
            };
        });
    };

    // 加载更多数据======================
    let count = 0;
    const loadMoreData = function loadMoreData() {
        let HTML = document.documentElement;
        // HTML.scrollTop+HTML.clientHeight+100 =>卷去的高度+可视窗口高度+100(误差)
        // HTML.scrollHeight页面真实的高度约等于
        if (HTML.scrollTop + HTML.clientHeight + 100 >= HTML.scrollHeight) {
            // 滚动到底部了
            count++;
            if (count > 5) return; //模拟加载5次,数据加载完不在进行处理
            queryData();
            binding();
            lazyImgsLoading();
        }
    };

    return {
        init() {
            queryData();
            binding();
            lazyImgsLoading();
            window.onscroll = throttle(() => {
                lazyImgsLoading();
                loadMoreData();
            }, 500); //滚动的时候 500MS
        }
    };
})();

fallsModel.init();

③最终版本 使用监听器IntersectionObserver

新版加载更多思路

新版加载更多数据.png



// 瀑布流的最终效果==================================================

let fallsModel = (function () {

    // 获取元素
    let fallsBox = document.querySelector('#fallsBox'),
        columns = Array.from(document.querySelectorAll('.column')),
        imgList = [],
        loadMoreBox=document.querySelector('.loadMoreBox');

    let data ;

    // IntersectionObserver 不兼容IE浏览器
    // 创建监听器 用来监听图片盒子和可视窗口的交叉状态
    let ob=new IntersectionObserver(changes=>{
        // 循环每个盒子监听到的交叉盒子,把出现在视口中的盒子做延迟加载
        changes.forEach(change=>{
            let{isIntersecting,target}=change;
            // target监听的盒子
            //isIntersecting 为 true 则是显示在了视口中
            // console.log(change);
            if(isIntersecting){
                // 当前盒子已经出现在视口中 target监听的盒子
                singleImgLazy(target.querySelector('img'));
                // 处理后 ,这个盒子就没必要再监听了
                ob.unobserve(target);
            }

        });
    },{
        // 控制完全出现在视口中 才算交叉状态改变
        threshold:[1]
    });





    // 1.从服务器获取数据==========================================================
    const queryData = function queryData() {
        let xhr = new XMLHttpRequest();
        xhr.open('GET', './data.json', false);
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4 && xhr.status === 200) {
                data = JSON.parse(xhr.responseText);
            };
        };
        xhr.send();
    };

    // 2.动态渲染数据=====================================================================================
    //瀑布流规则 每一次从数据中取出3条数据(group) 三列(colums)
    //三条数据,按照图片高度排序[小,中,大]三列
    //盒子,按照高度进行排序[大,中,小]

    const binding = function binding() {
        // 服务器获取到的图片宽度高度,按照真实渲染的宽度(230)动态计算出真实渲染的高度
        // 服务器获取到的宽高 width:300 height:433   300/433 宽高比 
        // 真实渲染的宽度为width:230px 求真实渲染的高度? 高度为 230/300/433

        // 用数组中的map方法
        data = data.map((item) => {
            let { width, height } = item;
            item.width = 230;
            item.height = 230 / (width / height);
            return item;//在把更改完的元素返回
        });

        let i = 0, //如果不想生成块级上下文 则写在外面 不想产生闭包
            group;
        for (; i < data.length; i += 3) {
            group = data.slice(i, i + 3); //从当前索引 取3项
            //i=0  0 1 2 
            //i=3  3 4 5
            group.sort((a, b) => {//按照图片高度 从小到大排序
                return a.height - b.height;
            });

            columns.sort((a, b) => {//按照盒子的高度 从大到小排序
                return b.offsetHeight - a.offsetHeight;
            });

            // 把排好序的数据依次插入到对应的排好序的列中
            group.forEach((item, index) => {
                let { pic, height, title, link } = item;
                // 动态创建每一个图片盒子 插入到指定列
                let box = document.createElement("div");
                box.className = 'item';
                box.innerHTML = `<a href="${link}" target="_blank">
                <div class="pic-box" style="height:${height}px;">
                    <img src="" alt="" data-img="${pic}">
                </div>
                <p class="desc-box">
                    ${title}
                </p>
            </a>`;
                // 插入到指定列
                columns[index].appendChild(box);
            });
        };

        // 数据绑定完成后,获取到所有的IMG
        imgList = Array.from(fallsBox.querySelectorAll('.pic-box img'));



    };


    // 3.图片的延迟加载============================================================
    //  +单张图片
    const singleImgLazy = function singleImgLazy(img) {
        // 传递的Img就是需要加载的图片对象
        let pic = img.getAttribute('data-img'),
            temp = new Image();//=>document.createElement('img')
        temp.src = pic;
        temp.onload = () => {
            //加载成功:图片地址是正确的
            img.src = pic;
            img.style.opacity = 1; //更改透明度  
        };
        // 处理过的IMG设置已经处理过的标识
        img.setAttribute('isLoad', 'TRUE');
    };




    // 循环图片集合
    const lazyImgsLoading = function lazyImgsLoading() {
        //ONSCROLL事件 会在浏览器最快反应事件内触发一次 (谷歌:5~7MS IE10~17MS) 这样频率太快了
        //  +我们需要降低它的频率,【方案:函数的节流】
        imgList.forEach(item => {
            // 已经处理过的IMG就无需判断了
            if (item.getAttribute('isLoad')) return;
            //把没有处理过的图片,它所在的盒子交给监听器去监听 
            ob.observe(item.parentNode);
        });
    };



    // 加载更多数据======================

    const loadMoreData = function loadMoreData() {
        let count = 0;
        let ob2=new IntersectionObserver(changes=>{

            let change=changes[0];
            if(change.isIntersecting){
                count++;
                if(count>5){
                    // 已经加载完数据了
                    ob2.unobserve(loadMoreBox);
                    return;
                }
                queryData();
                binding();
                lazyImgsLoading();
            }
        });
        ob2.observe(loadMoreBox);    
    };

    return {
        init() {
            queryData();
            binding();
            lazyImgsLoading(); //延迟加载
            loadMoreData();

        }
    };
})();

fallsModel.init();

jQuery方案

let fallModel=(function(){

    let $fallsBox=$('#fallsBox'),
        $colums=$fallsBox.children('.column'),
        $loadMoreBox=$('.loadMoreBox'),
        $imglist=null,
        data=[];

    // 获取数据
    const queryData=function queryData(){
        $.ajax({
            url:'./data.json',
            method:"GET", //method和type一样 之前的是 type
            async:false,
            dataType:'json',
            success(result){
                data=result;
            }
        });
    };

    // 渲染数据
    const binding=function binding(){
        data=data.map(item=>{
            let {width,height}=item;
            item.width=230;
            item.height=230/(width/height);
            return item;
        });

        let i=0,
            group;
        for(;i<data.length;i+=3){
            group=data.slice(i,i+3);
            if(i>0){
                // 从第二次开始,再把数据和列排序
                group.sort((a,b)=>a.height-b.height);
                // $colums类数组 jQuery=>sort 不改变原始数组
                $colums=$colums.sort((a,b)=>b.offsetHeight-a.offsetHeight);
            }
            group.forEach((item,index)=>{
                let {pic,height,title,link}=item;
                $(`<div class="item">
                    <a href="${link}" target="_blank">
                        <div class="pic-box" style="height:${height}px">
                            <img src="" alt="" data-img="${pic}">
                        </div>
                        <p class="desc-box">
                            ${title};
                        </p>
                    </a>
                </div>`).appendTo($colums[index]); //子元素插入到父元素
            });
        };


        // 获取IMG 进行监听
        $imglist=$fallsBox.find('img[data-img]');

        console.log($imglist);
      // item 原生DOM对象
        $imglist.each((_,item)=>ob.observe(item.parentNode));
    };




    //图片延迟加载
    let config={
        threshold:[1]
    };
    let ob=new IntersectionObserver(changes=>{
        changes.forEach(item=>{
            let {isIntersecting,target}=item;
            if(isIntersecting){
                singleImgLazy($(target).children('img'));
                ob.unobserve(target);
            }
        });
    },config);


     // 单张延迟加载
     const singleImgLazy=function singleImgLazy($img){
        let pic=$img.attr('data-img'),
            temp=new Image;
            temp.src=pic;
            temp.onload=()=>{
                $img.attr('src',pic);
                $img.css({
                    opacity:1
                });
            };
        $img.removeAttr('data-img');
     };


    // 加载更多
    const loadMoreData=function loadMoreData(){
        let count=0;
        let ob=new IntersectionObserver(changes=>{
            let change=changes[0];
            if(change.isIntersecting){
                count++;
                if(count>5){
                    ob.unobserve($loadMoreBox[0]);
                    return;
                }
                queryData();
                binding();
            }
        });
        ob.observe($loadMoreBox[0]);
    };

    return{
        init(){
            queryData();
            binding();
            loadMoreData();
        }
    };
})();

fallModel.init();