# 渲染10w长度列表解决方案

# 开头

之前写了一篇长列表卡顿优化,由于比较着急,紧急解决了问题,文章也指出了不足之处。

虽然当前场景基本解决了,但明显普适性不够,缺点非常明显:
- 1.每列必须固定高度
- 2.如果页面因为一些交互导致整个页面高度变了,需要重置每个列的offsetTop
- 3.并不是真正意义的无限,毕竟每增加一列,还是会增加一个div

好巧不巧,另外一个系统又出现这种问题,还是B端的,商品sku排列组合,很容易组合出了上万条数据,浏览器变得非常卡。这次换了新的,真正支持无限滚动的方案,下面的抽出来的核心逻辑 demo (vue2.0版)


<template>
  <div class="box">
    <div :style="{ height: topHeight + 'px' }" class="top"></div>
    <div
      class="item"
      :style="{ height: itemHeight + 'px' }"
      :key="item"
      v-for="item in list"
    >
      {{ item }}
    </div>
    <div :style="{ height: bottomHeight + 'px' }" class="bottom"></div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      originList: new Array(100000).fill(0).map((it, i) => i + 1),
      list: [],
      maxItems: 0,
      topHeight: 0,
      bottomHeight: 0,
      itemHeight: 60
    };
  },
  mounted() {
    this.maxItems = Math.ceil(
      document.documentElement.clientHeight / this.itemHeight
    );
    if (this.originList.length <= this.maxItems) {
      this.list = this.originList;
      return;
    }
    window.addEventListener("scroll", this.calculate);
    window.addEventListener("resize", this.resize);
    this.calculate();
  },
  methods: {
    resize() {
      this.maxItems = Math.ceil(
        document.documentElement.clientHeight / this.itemHeight
      );
      this.calculate();
    },
    calculate() {
      const scrollTop = document.documentElement.scrollTop;
      const len = this.originList.length;
      let start = 0;
      let end = 0;

      start = Math.floor((scrollTop - this.$el.offsetTop) / this.itemHeight);
      if (start + this.maxItems > len) {
        start = len - this.maxItems;
      }
      end = start + this.maxItems;

      this.list = this.originList.slice(start, end);

      this.topHeight = start * this.itemHeight;

      this.bottomHeight =
        this.originList.length * this.itemHeight -
        (this.maxItems * this.itemHeight + this.topHeight);
      console.log(
        `start`,
        start,
        `end`,
        end,
        "topHeight",
        this.topHeight,
        "bottomHeight",
        this.bottomHeight
      );
    }
  },
  beforeDestroy() {
    window.removeEventListener("scroll", this.calculate);
    window.removeEventListener("resize", this.resize);
  }
};
</script>
<style>
* {
  padding: 0;
  margin: 0;
}
.item {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  border: 1px solid #ccc;
  color: blueviolet;
}
</style>


# 说下过程

事实上解决这次的性能问题过程比 demo 体现出来的看起来复杂得多。

首先,我接到的问题描述十分精炼

你看看这个页面怎么这么卡?

我一点开,等了5分钟,得亏浏览器没有奔溃,我才看到一点页面头子,想滚动还不行,滚一下卡几十秒,完全不能分析,只能硬啃代码。

还好性能优化是有门道的,从页面白屏时间远远超出请求时间来看,绝对不光是渲染列表问题,大概率还有 js 耗时问题。

代码打开就注意到了显眼的双重 forEach

虽然列表长度只有 1000 多,双循环直接就变成了执行 1000^2 次!

这种复杂度O(n2)的写法真应该禁止掉

最开始想着怎么快怎么解决,结果只是换成了 for ,再加上一些条件下 break,这样一来确实减少了函数创建时间和循环执行次数,然而明显 算法复杂度没有变,并没有什么卵用。

索性换了算法,把其中一个循环体换成了 hash ,用空间换来了时间

然后才进入到长列表渲染问题...