1

前言

  • 最近在学习瀑布流的实现方法,网上找了许多实现的方法,但发现与自己的需求不太符合,于是对代码进行了一些改动。
  • 个人需求:
    1. 横向加载

    1. 服务器返回的信息中没有图片尺寸
    2. 每加载完一张图片就设置它的位置,而不是等所有图片都显示完再布局
  • 性能肯定会比较慢,最好的方法还是在服务器返回图片尺寸大小或比例。
  • 为了方便操作,采用了在html中引用Vue.js的方法实现。

效果预览

  • 为了更直观的展示,这里设置了3G网络并禁用了缓存

image

  • 正常网速

image

主要改动地方

  • 根据盒子宽度设置列数 => 根据列数设置box的宽度
    改动原因是根据列数设置宽度我觉得会更直观方便,而且右侧不会留下空白
// 瀑布流布局
waterFall() {

    ...

    const columns = 3  // 列数
    const gap = 10;  // 间隔
    const itemWidth = ~~((this.getClient().width / columns - gap))

    ...
},
// 获取页面宽度
getClient() {
    const SCROLL_WIDTH = 20
    return {
        /*
            ** window.innerWidth - SCROLL_WIDTH: 页面宽度(包括滚动条)- 比滚动条多一点的宽度
            ** 不使用document.body.clientWidth 和 document.documentElement.clientWidth原因:
            ** 页面首次加载时的宽度没有被滚动条挤压的,这样会导致滚动加载时获取的页面宽度与首次的宽度不一致
            ** 一般浏览器滚动条宽度为17px,减去20是保守估计并为右侧留一定空间
            ** 移动端滚动条是悬浮在页面在上,不会造成挤压,因此在移动端中可以不减去滚动条宽度
        */
        width: window.innerWidth - SCROLL_WIDTH
        // width: window.innerWidth || document.body.clientWidth || document.documentElement.clientWidth
    }
},
// 对box进行布局
reflow(el, itemWidth, columns, gap) {
    
    el.style.width  = itemWidth + 'px'

    ...
}
  • HTML文档加载完毕后再统一布局 => 加载完一张图片就布局一次
    (window.onload => image.onload)
// 瀑布流布局
waterFall() {

    const columns = 3  // 列数
    const gap = 10;  // 间隔
    const itemWidth = ~~((this.getClient().width / columns - gap))

    const box = document.getElementById("big-box")
    const items = box.children

    for (let i = this.loadCount; i < items.length; i++, this.loadCount++) {

        // 获取图片元素
        const img = items[i].getElementsByTagName('img')[0]
        // 图片有缓存时直接布局(主要在窗口尺寸变化时调用)
        if(img.complete) {
            this.reflow(items[i], itemWidth, columns, gap)
        }
        // 图片无缓存时先对加载速度快的图片进行布局
        else {
            img.addEventListener("load", () => {
                this.reflow(items[i], itemWidth, columns, gap)
            })
        }

    }
},
// 对box进行布局
reflow(el, itemWidth, columns, gap) {
    el.style.width  = itemWidth + 'px'

    // 第一行
    if (this.arr.length < columns) {
        el.style.top = 0;
        el.style.left = (itemWidth + gap) * this.arr.length + 'px'
        this.arr.push(el.offsetHeight)
    }
    // 其他行
    else {
        // 最小的列高度
        const minHeight = Math.min(...this.arr)
        // 当前高度最小的列下标
        const index = this.arr.indexOf(minHeight)

        el.style.top = minHeight + gap + 'px'
        el.style.left = (itemWidth + gap) * index + 'px'

        this.arr[index] = this.arr[index] + el.offsetHeight + gap
    }
}

完整代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>

    <title>JS 实现瀑布流(Vue)</title>
    <style>
        .container {
            position: relative;
        }

        .box {
            position: absolute;
            width: 0;  /* 设置0是为了布局时才显示图片,防止看到图片都在第一张的位置上堆叠 */
        }

        .box img {
            width: 100%;
        }
    </style>

</head>

<body>
    <div id="app">
        <div id="big-box" class="container">
            <div class="box" v-for="(item, index) in pic_list" :key="index">
                <img :src="item">
            </div>
        </div>
    </div>

    <script>
        new Vue({
            el: '#app',
            name: 'WaterFall',
            data() {
                return {
                    isReSize: false,  // 窗口尺寸是否发生变化
                    lock: true,  // 锁
                    pic_list: [],
                    arr: [],  // 存放每一列的最小高度 
                    loadCount: 0  // 已经布局好的元素下标
                }
            },
            methods: {
                 // 瀑布流布局
                waterFall() {

                    const columns = 3  // 列数
                    const gap = 10;  // 间隔
                    const itemWidth = ~~((this.getClient().width / columns - gap))
                    // console.log(this.getClient().width, itemWidth)

                    const box = document.getElementById("big-box")
                    const items = box.children
                    // console.log(items)

                    // 窗口尺寸发生变化时,全部box重新布局
                    if(this.isReSize) {
                        this.loadCount = 0
                        this.arr = []
                        this.isReSize = false
                    }

                    for (let i = this.loadCount; i < items.length; i++, this.loadCount++) {

                        // 获取图片元素
                        const img = items[i].getElementsByTagName('img')[0]
                        // 图片有缓存时直接布局(主要在窗口尺寸变化时调用)
                        if(img.complete) {
                            this.reflow(items[i], itemWidth, columns, gap)
                        }
                        // 图片无缓存时先对加载速度快的图片进行布局
                        else {
                            img.addEventListener("load", () => {
                                this.reflow(items[i], itemWidth, columns, gap)
                            })
                        }

                    }
                    console.log('-------------------------------')
                },
                // 对box进行布局
                reflow(el, itemWidth, columns, gap) {
                    el.style.width  = itemWidth + 'px'

                    // 第一行
                    if (this.arr.length < columns) {
                        el.style.top = 0;
                        el.style.left = (itemWidth + gap) * this.arr.length + 'px'
                        this.arr.push(el.offsetHeight)
                    }
                    // 其他行
                    else {
                        // 最小的列高度
                        const minHeight = Math.min(...this.arr)
                        // 当前高度最小的列下标
                        const index = this.arr.indexOf(minHeight)
                        // console.log(index, minHeight)

                        el.style.top = minHeight + gap + 'px'
                        el.style.left = (itemWidth + gap) * index + 'px'

                        this.arr[index] = this.arr[index] + el.offsetHeight + gap
                    }
                    // console.log(JSON.parse(JSON.stringify(this.arr)))
                },
                // 获取页面宽度
                getClient() {
                    const SCROLL_WIDTH = 20
                    return {
                        /*
                         ** window.innerWidth - SCROLL_WIDTH: 页面宽度(包括滚动条)- 比滚动条多一点的宽度
                         ** 不使用document.body.clientWidth 和 document.documentElement.clientWidth原因:
                         ** 页面首次加载时的宽度没有被滚动条挤压的,这样会导致滚动加载时获取的页面宽度与首次的宽度不一致
                         ** 一般浏览器滚动条宽度为17px,减去20是保守估计并为右侧留一定空间
                         ** 移动端滚动条是悬浮在页面在上,不会造成挤压,因此在移动端中可以不减去滚动条宽度
                        */
                        width: window.innerWidth - SCROLL_WIDTH
                        // width: window.innerWidth || document.body.clientWidth || document.documentElement.clientWidth
                    }
                },
                 // 延迟函数,防止短时间内执行多次
                wait(func, time=300) {
                    if(this.lock) {
                        this.lock = false
                        setTimeout(() => {
                            func()
                            this.lock = true
                        }, time)
                    }
                },
                async getPic() {
                    // const {data: res} = await axios.get('http://127.0.0.1:5000/pics')

                    // 通过访问网络资源模拟获取图片路径
                    res = [
                        "https://pic.rmb.bdstatic.com/932ca9934baf734b3160fc76bc761dcf.jpeg",
                        "https://ss2.baidu.com/-vo3dSag_xI4khGko9WTAnF6hhy/zhidao/pic/item/b7003af33a87e9500080e13912385343faf2b4cb.jpg",
                        "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2529255061,3898604095&fm=26&gp=0.jpg",
                        "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3376008599,2577895433&fm=26&gp=0.jpg",
                        "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2538164638,2454489739&fm=26&gp=0.jpg",
                        "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=235101227,1662203838&fm=26&gp=0.jpg",
                        "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3459144041,1901798922&fm=11&gp=0.jpg",
                        "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=3379363998,2377527589&fm=26&gp=0.jpg",
                        "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=4123338137,2748046536&fm=11&gp=0.jpg",
                        "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3116135450,3184020205&fm=11&gp=0.jpg",
                        "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4080806858,1854570161&fm=11&gp=0.jpg",
                        "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1949459665,247597437&fm=11&gp=0.jpg",
                        "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=1433933789,3443668472&fm=26&gp=0.jpg",
                        "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2499198753,3249918221&fm=26&gp=0.jpg",
                        "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2659998876,1549965278&fm=15&gp=0.jpg",
                        "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3459729031,3827366337&fm=26&gp=0.jpg",
                        "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2635451446,3680267750&fm=26&gp=0.jpg",
                        "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1133790732,3263952126&fm=26&gp=0.jpg",
                        "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3375224907,3777918150&fm=26&gp=0.jpg",
                        "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2372825572,3775270174&fm=26&gp=0.jpg",
                    ]

                    this.pic_list.push(...res)
                    await this.$nextTick()
                    this.waterFall()
                }
            },
            mounted() {
                this.getPic()
                // 窗口尺寸变化事件
                window.addEventListener("resize", () => {
                    this.isReSize = true
                    this.wait(this.waterFall)
                })
                // 窗口滚动事件
                window.addEventListener("scroll", () => {
                    this.wait(() => {
                        // 是否滚动到底部
                        const IS_BOTTOM = document.documentElement.scrollHeight - document.documentElement.scrollTop <= document.documentElement.clientHeight
                        if(IS_BOTTOM) {
                            this.getPic()
                            console.log('到底了')
                        }
                    })
                })
            }
        })
    </script>
</body>

</html>

存在的问题

  • 目前图片的尺寸获取和位置设置都是在image的onload方法中执行的,效率会比较慢
  • 窗口尺寸改变时会对所有的图片进行重新布局(可以考虑加个数组按图片加载顺序记录下标)

参考文章


twips_mio
165 声望0 粉丝