vue 瀑布流的实现 - 使用 nexttick 协助计算dom高度

1,249 阅读2分钟

瀑布流布局在业务中很常见,解决方案也有比较多,今天给大家介绍一种基于dom高度自动计算的实现方案

先看效果图

image.png

我们的列表是由图片和文字组成,一般的,图片是确定高度的(就算不清楚我们也可以临时加载然后判断),此处的文本内容要求是必须全部展示出来,不做切断处理,因此可能存在文本较少的情况,此时的卡片布局会相对的减少

我们将列表分为左右两列,用来单独放置卡片

image.png

<template>
  <div class="WaterfallList">
    <!-- 左侧列表 -->
    <div class="WaterfallList-left">
      <div ref="left">
        <div class="card" v-for="(item, $index) in left" :key="$index">
          <div
            class="pic"
            :style="{
              height: item.height + 'px',
              lineHeight: item.height + 'px',
            }"
          >
            {{ item.index }}
          </div>
          <div class="title">{{ item.title }}</div>
        </div>
      </div>
    </div>
    <!-- 右侧列表 -->
    <div class="WaterfallList-right">
      <div ref="right">
        <div class="card" v-for="(item, $index) in right" :key="$index">
          <div
            class="pic"
            :style="{
              height: item.height + 'px',
              lineHeight: item.height + 'px',
            }"
          >
            {{ item.index }}
          </div>
          <div class="title">{{ item.title }}</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    list: Array,
  },

  data() {
    return {
      left: [],
      right: [],
    };
  }
};
</script>


因此,在渲染的时候我们需要获得当前左右列表的高度,然后将新的数据塞入到较小的那个容器之中

const newItem; // 下一个要插入的数据

let leftHeight = this.$refs.left.getBoundingClientRect().height;
let rightHeight = this.$refs.right.getBoundingClientRect().height;
let index = this.left.length + this.right.length;

if (leftHeight < rightHeight) {
  this.left.push(newItem);
} else {
  this.right.push(newItem);
}

由于使用了vue框架,每次进行数据变动后,dom并不一定会马上做出新的改变,因此可能导致上面的获取高度的代码并不能获取到渲染完成后的,因此,我们需要等待dom完成渲染, 通过如下代码实现

await this.$nextTick()

这样,我们在每次list放生改变的时候,就需要将新增的内容按情况推进我们的左右两个数组中

epxort default {
  watch: {
    list() {
      this.update();
    },
  },

  mounted() {
    this.update();
  },

  methods: {
    async update() {
      while (this.left.length + this.right.length !== this.list.length) {
        let leftHeight = this.$refs.left.getBoundingClientRect().height;
        let rightHeight = this.$refs.right.getBoundingClientRect().height;
        let index = this.left.length + this.right.length;

        if (leftHeight < rightHeight) {
          this.left.push(this.list[index]);
        } else {
          this.right.push(this.list[index]);
        }

        await this.$nextTick();
      }
    },
  }
}

考虑到如果watch频繁触发会导致 update 方法多次调用,而此时前一次 update 工作并不一定完成,因此我们加一个锁来保证它不会重入

if (this.called) {
    return;
}

this.called = true;
while (this.left.length + this.right.length !== this.list.length) {
    let leftHeight = this.$refs.left.getBoundingClientRect().height;
    let rightHeight = this.$refs.right.getBoundingClientRect().height;
    let index = this.left.length + this.right.length;

    if (leftHeight < rightHeight) {
      this.left.push(this.list[index]);
    } else {
      this.right.push(this.list[index]);
    }

    await this.$nextTick();
}
this.called = false;

下面是完整的项目代码 waterfallList-calc-by-$nexttick - CodeSandbox

那么问题来了,如果列表发生删减怎么操作呢,我们可以

  1. 先把已经不存在 list 中的元素,从 left 和 right 中过滤出去
  2. 过滤完后,左右的高度可能差别很大,我们做一个循环,每次从较大的拿一列中取出最后一项,放到较小的里面,直到较大的那列不在大于较小的那列(移动过程中高度会发生变化)
  3. 此时继续上面的循环,将新增的内容按情况插入到左右两个