【性能优化】初学者可以弄得懂的虚拟滚动列表(入门级)

403 阅读2分钟

前言

长列表问题在项目中已经遇到很多次了,社区中也有很多优秀的文章,这次终于在b站大佬的视频里入门了,下面是一篇学习总结,初学者推荐去看这个视频,保证可以弄得懂!!

准备

1.用node来搭建服务,模拟前后端接口请求效果(后端代码直接用大佬的)

2.前端先写几个简单的样式

分析

简答的分析一哈嘛

打个比方说一哈就是,平时我们用设备浏览新闻之类的数据,对于我们来说滑动到当前设备可视区中看到的的东西对于我们来说才是“有效的”,而其他地方反正我们也看不到,我们才不用关心可视区之外的数据是否加载出来,只要滑动到可视区的时候,可视区的内容给我们显示出来就好了。

文字太枯燥 上个图来试试(ps 图片来源于网络)

虚拟滚动实现

由于渲染成页面的时候需要解析数据生成dom树,cssom树然后dom树和cssom树最后会合成render树,最终渲染生成可是界面(这个过程不懂的话推荐看这一篇),如果数据量很大的话,这个过程会很慢,性能也是比较低的,假设不论有多少条数据,我们就是很任性就是只加载可视区里面的几条数据,性能就会得到优化。 要做到上面说的“任性”,那自然需要我们人为的处理才行。

要囊个处理嘛?

这里的核心问题就是要在返回的数据里面,找到当前应该在屏幕中渲染的数据。

阔以上代码试一哈了

初始化-先把数据模拟出来(代码见代码-附1)

这里没得啥子好说的,把数据从后台请求,然后渲染到页面中就可以了。

第二步-需要定义几个很重要的变量

  • oneItemHeight: 每一条数据占的高度。
  • containerHeight:拿到滚动的高度。
  • maxContainerSize:屏幕中可以显示的最大的数据条数。
  • containerHeight / oneItemHeight = maxContainerSize
  • 在不断滚动的过程中需要知道滚动容器最上方显示的数据在所有数据中的位置
  • 同理:也需要知道在滚动容器最下方显示的数据在所有数据中的位置

计算maxContainerSize(注意边界情况,最上面和最下面露出来一点点)

  getContainSize() {
      debugger
      //1.第一步:计算容器盒子的高度
      let height = this.$refs.scrollContainer.offsetHeight;
      //3.第二步:整个外面盒子的高度 / 每一个的高度 = 每一屏可以加载的最大条数
      let num = height / this.oneItemHeight;
      //4.第三步:要注意:如果上面露出了一点点,下面漏出来一点点  那么当前的数量就是上面出得整数+2
      let maxNum = Math.floor(num) + 2;

      this.maxContainerSize = maxNum;
      console.log(this.maxContainerSize)
    }

计算startIndex

startIndex初始值为0,边滚动的时候startIndex的值也会发生变化 伪代码计算过程如下

startIndex = 滚出去的数据条数-1;
滚出去的数据条数 = 滚出去的高度 / 每一条数据的高度
  //计算滚出去的数据
    setStartIndex() {
      let scrollTop = this.$refs.scrollContainer.scrollTop;//当前滚出去的高度
      let currentIndex = Math.floor(
        this.$refs.scrollContainer.scrollTop / this.oneItemHeight //滚出去的数据条数
      );
      if (currentIndex == this.startIndex) {
        return;
      }
      this.startIndex = currentIndex;
      console.log(currentIndex, "=======");
    },
    //监听滚动方法的时候,计算startIndex的值
    handleScroll() {
      this.setStartIndex();
    }

根据startIndex我们可以用计算属性计算出endIndex

  • endIndex: startIndex + 容器最大条数(但是要考虑特殊情况,当前显示的是最后一屏,并且最后一屏的数据不足整屏的时候,endIndex的值为数据的长度-1)
 endIndex() {
      let endIndex = this.startIndex + this.maxContainerSize; //9第九步:要注意的是容器的最下面可能是空白的,此时需要我们做一次判断
     if (endIndex > this.listData.length) {//考虑特殊情况:最后一屏的数据不满整个屏幕的时候
        endIndex = this.allDataList.length - 1; //这个时候已经滚动到最底部了 
      }
      return endIndex;
    },
  • 看看下面这个gif来感受一哈

Animation.gif

  • 再来看看:下面为了演示startIndex和endIndex的变化,我用了watch监听 用computed有缓存 Animation.gif

startIndex和endIndex都计算出来的,然后去请求的数据里面截取数据就可以了

现在页面上只显示了8条数据,此时我们也会发现页面上永远最多只能显示整个页面的最大条数,而数据最多只能滚出去一条

     //10第十步:定义一个带显示的数组列表元素,从获取的数组里面截取
    showDataList() {
      return this.listData.slice(this.startIndex,this.endIndex)
    },

为了解决上面的问题,我们需要将滚出去的高度撑开

这里借助padding 其实margin也是可以的 改造一下html,给滚动列表外面加一个盒子,用盒子的padding将最外层的盒子的scrollTop撑开

image.png 先试一下加padding是不受有用

image.png image.png 在这里需要如下几步处理:

  • 每次滚动到最底部的时候需要请求一次数据,将请求出来的新旧数据追加
  • 计算padding 撑开的scrollTop
 //监听滚动方法的时候,计算startIndex的值
    async handleScroll() {
      this.setStartIndex();
      //第十四步:需要实现下拉加载分页功能:什么时候触发下拉加载功能呢,startIndex + contentSize > data.length-1
      console.log(
        this.startIndex,
        this.maxContainerSize,
        this.showDataList.length
      );
      if (
        this.startIndex + this.maxContainerSize >=
        this.listData.length - 1
      ) {
        console.log("滚动到了底部");
        //就追加数据
        let request = await this.getListData(20);

        this.listData = [...request, ...this.listData]; //重新请求数据,新旧数据追加
        console.log(this.listData);
      }
    }

padding计算

 //11第十一步:需要定义一个上空白的高度
    topBlankFill() {
      return this.startIndex * this.oneItemHeight;
    },
    //12第十二步:需要定义上空白的高度
    bottomBlankFill() {
      return (this.listData.length - this.endIndex) * this.oneItemHeight;
    }

看看效果 一直在下拉加载数据,但是右边的dom永远只有8条数据

Animation.gif

到这里虚拟滚动的核心代码已经实现完了,其实也没有很难的样子,啊哈哈(此步骤代码下面见附-3)

优化

说完了,其实还没有完,还有好多比较重要的地方没有优化

优化一:节流防抖优化频繁触发的滚动事件

利用防抖原理:每隔500s才触发一次scroll函数里面的逻辑

 //监听滚动方法的时候,计算startIndex的值
    handleScroll() {
      this.debounce(this.scrollFun, 500);
    },
    debounce(fn, delay) {
      clearTimeout(this.isScrollStatus);
      this.isScrollStatus = setTimeout(function() {
        fn();
      }, delay);
    },
    async scrollFun() {
      console.log("滚动了");
      this.setStartIndex();
      if (this.startIndex + this.maxContainerSize >= this.listData.length - 1) {
        //就追加数据
        let request = await this.getListData(20);
        this.listData = [...request, ...this.listData];
      }
    }

优化方案二 利用requestAnimationFrame来优化动画

MDN的解释如下:

你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
requestAnimationFrame对比setTimeout来说他每一次的动画渲染都是连贯的,他必须要等到这一次的渲染完成才会渲染下一个动画,而setTimeout则在1000ms之后不管上一次的动画是否渲染完成都会重新渲染

通俗一点就是哈,requestAnimationFrame对比setTimeout来说他每一次的动画渲染都是连贯的,他必须要等到这一次的渲染完成才会渲染下一个动画,而setTimeout则在1000ms之后不管上一次的动画是否渲染完成都会重新渲染。

requestF() {
      let requestAnimationFrame =
        window.requestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.msRequestAnimationFrame;
      //浏览器防抖优化:根据浏览器FPS采用递归方法,队列调用requestAnimationFrame方法实现优化
      let fps = 30;
      let interval = 1000 / fps;
      let then = Date.now();
      requestAnimationFrame(() => {
        let now = Date.now();
        let delta = now - then;
        then = now;
        this.scrollFun();
        if (delta >= interval) {
          requestAnimationFrame(arguments.callee);
        }
      });
    },

优化三 :需要在最上面和最下面增加一屏缓冲区

模拟不出来了:脑补一哈,如果第二次请求的时候网络很慢,但是我们已经滚动到最底部了,但是数据还没有请求出来,这个时候我们可能会看到下面一片空白区,所以我们要在最上面和最下面分别留出一屏,来解决这个问题 重新计算endIndex

 endIndex() {
      let endIndex = this.startIndex + this.containSize * 2; //9第九步:给最下面预留缓冲
      if (endIndex > this.allDataList.length) {
        endIndex = this.allDataList.length - 1;
      }
      return endIndex;
    }
    

showList:在最上方和下方预留

 //10第十步:定义一个带显示的数组列表元素,从获取的数组里面截取
    showDataList() {
      let startIndex = 0;
      let endIndex = 0;
      if (this.startIndex <= this.containSize) {
        //这个时候上面还没有滚出去
        startIndex = 0;
      } else {
        startIndex = this.startIndex - this.containSize * 2; //需要减去滚两屏的数据
      }
      return this.listData.slice(startIndex, this.endIndex);
    },

topPadding:也需要重新计算,需要减去预留的那一屏

//11第十一步:需要定义一个上空白的高度
    topBlankFill() {
      //20.第二十步:上下的padding也需要
      //第十九步:设置前后缓冲区
      let paddingTop = 0;
      if (this.startIndex > this.maxContainerSize) {
        //已经滚出去一屏
        // 当当前滚动位置大于屏幕容积后才填充空白
        paddingTop =
          (this.startIndex - this.maxContainerSize) * this.oneItemHeight;
        console.log(this.startIndex, this.maxContainerSize,paddingTop);
      }
      return paddingTop;
    },

阔以啦:见附-4:还有一些细节没处理,推荐去看视频,很详细

代码

附-1:初始化

<template>
  <div class="news-box">
    <!--第七步:监听滚动事件-->
    <div class="scroll-container" ref="scrollContainer">
      <div v-for="(item, index) in listData" :key="index">
        <router-link class="one-new" :to=" '/article/' + item.title + '/'+ item.reads + '/'+ item.from + '/'+ item.date + '/'+ item.image ">
          <!-- 新闻左侧标题、评论、来源部分 -->
          <div class="new-left">
            <h3>{{ item.title }}</h3>
            <div>
              <p>
                <img src="../assets/icons/msg.png" alt="评" />
                <span>{{ item.reads }}</span>
                <span>{{ item.from }}</span>
              </p>
              <h4>{{ item.date }}</h4>
            </div>
          </div>
          <!-- 新闻右侧图片部分 -->
          <div class="new-right">
            <!-- <img :src="imgsList[oneItem.image]" alt="PIC" /> -->
          </div>
        </router-link>
      </div>
    </div>
  </div>
</template>

<script>
// 引入准备好的新闻图片相关信息
import imgsList from "../components/newsImgs.js";
import { setTimeout, clearTimeout } from "timers";
export default {
  data() {
    return {
      listData: []
    };
  },
  created() {
    let num = this.getListData(20);
    console.log(num, "=========");
  },
  methods: {
    getListData(num) {
      return this.$axios
        .get("http://localhost:4000/data?num=" + num)
        .then(res => {
          this.listData = res.data.list;
        })
        .catch(() => {
          this.msg = "请求失败稍后请重试...";
          return false;
        });
    }
  }
};
</script>

<style lang="scss" scoped>
.news-box {
  width: 100%;
  max-width: 800px;
  height: 100%;
  .scroll-container {
    width: 100%;
    height: 100%;
    overflow-y: auto;
    .one-new {
      text-decoration: none;
      display: block;
      display: flex;
      flex-direction: row;
      flex-wrap: nowrap;
      justify-content: space-between;
      border-bottom: 1px solid #ddd;
      padding: 14px 10px 5px;
      .new-left {
        height: 80px;
        position: relative;
        h3 {
          padding: 0;
          margin: 0;
          font-size: 16px;
          text-align: justify;
          color: #555;
        }
        div {
          position: absolute;
          width: 100%;
          bottom: 10px;
          display: flex;
          flex-direction: row;
          flex-wrap: nowrap;
          justify-content: space-between;
          align-items: center;
          p {
            display: flex;
            flex-direction: row;
            flex-wrap: nowrap;
            justify-content: space-between;
            align-items: center;
            img {
              height: 16px;
            }
            span {
              font-size: 12px;
              color: #555;
              margin-left: 3px;
              margin-right: 3px;
            }
          }
          h4 {
            font-size: 12px;
            color: #888;
          }
        }
      }
      .new-right {
        margin-left: 10px;
        img {
          height: 68px;
        }
      }
    }
    .msg h2 {
      font-size: 18px;
      text-align: center;
      color: #666;
      padding-top: 58px;
    }
  }
}
</style>

附-2 计算滚动过程中的startIndex

<template>
  <div class="news-box">
    <!--第七步:监听滚动事件-->
    <div class="scroll-container" ref="scrollContainer" @scroll.passive="handleScroll">
      <div v-for="(item, index) in showDataList" :key="index">
        <router-link class="one-new" :to=" '/article/' + item.title + '/'+ item.reads + '/'+ item.from + '/'+ item.date + '/'+ item.image ">
          <!-- 新闻左侧标题、评论、来源部分 -->
          <div class="new-left">
            <h3>【{{index}}】{{ item.title }}</h3>
            <div>
              <p>
                <img src="../assets/icons/msg.png" alt="评" />
                <span>{{ item.reads }}</span>
                <span>{{ item.from }}</span>
              </p>
              <h4>{{ item.date }}</h4>
            </div>
          </div>
          <!-- 新闻右侧图片部分 -->
          <div class="new-right">
            <!-- <img :src="imgsList[oneItem.image]" alt="PIC" /> -->
          </div>
        </router-link>
      </div>
    </div>
  </div>
</template>

<script>
// 引入准备好的新闻图片相关信息
import imgsList from "../components/newsImgs.js";
import { setTimeout, clearTimeout } from "timers";
export default {
  data() {
    return {
      listData: [],
      oneItemHeight: 100, //2.第二步:每一个的高度border + padding +width
      containerHeight: 0,
      maxContainerSize: 0, //屏幕中可以显示的最大的数据条数
      startIndex: 0
      // endIndex: 0
    };
  },
  created() {
    this.getListData(20);
  },
  mounted() {
    this.getContainSize();
  },
  computed: {
    endIndex() {
      let endIndex = this.startIndex + this.maxContainerSize; //9第九步:要注意的是容器的最下面可能是空白的,此时需要我们做一次判断
      if (endIndex > this.listData.length) {
        //考虑特殊情况:最后一屏的数据不满整个屏幕的时候
        endIndex = this.listData.length - 1; //这个时候已经滚动到最底部了
      }
      console.log(endIndex, "========endIndex=========");
      console.log(this.maxContainerSize, "========maxContainerSize=========");
      return endIndex;
    },
    //10第十步:定义一个带显示的数组列表元素,从获取的数组里面截取
    showDataList() {

      return this.listData.slice(this.startIndex,this.endIndex)
    },
  },
  watch: {
    
  },
  methods: {
    getListData(num) {
      return this.$axios
        .get("http://localhost:4000/data?num=" + num)
        .then(res => {
          this.listData = res.data.list;
        })
        .catch(() => {
          this.msg = "请求失败稍后请重试...";
          return false;
        });
    },

    getContainSize() {
      //1.第一步:计算容器盒子的高度
      let height = this.$refs.scrollContainer.offsetHeight;
      //3.第三步:整个外面盒子的高度 / 每一个的高度 = 每一屏可以加载的最大条数
      let num = height / this.oneItemHeight;
      //4.第四步:要注意:如果上面露出了一点点,下面漏出来一点点  那么当前的数量就是上面出得整数+2
      let maxNum = Math.floor(num) + 2;

      this.maxContainerSize = maxNum;
      console.log(this.maxContainerSize);
    },
    //计算滚出去的数据
    setStartIndex() {
      let scrollTop = this.$refs.scrollContainer.scrollTop; //当前滚出去的高度
      let currentIndex = Math.floor(
        this.$refs.scrollContainer.scrollTop / this.oneItemHeight //滚出去的数据条数
      );
      if (currentIndex == this.startIndex) {
        return;
      }
      this.startIndex = currentIndex;
      console.log(this.startIndex, "===startIndex====");
    },
    //监听滚动方法的时候,计算startIndex的值
    handleScroll() {
      this.setStartIndex();
    }
  }
};
</script>

<style lang="scss" scoped>
.news-box {
  width: 100%;
  max-width: 800px;
  height: 100%;
  .scroll-container {
    width: 100%;
    height: 100%;
    overflow-y: auto;
    .one-new {
      text-decoration: none;
      display: block;
      display: flex;
      flex-direction: row;
      flex-wrap: nowrap;
      justify-content: space-between;
      border-bottom: 1px solid #ddd;
      padding: 14px 10px 5px;
      .new-left {
        height: 80px;
        position: relative;
        h3 {
          padding: 0;
          margin: 0;
          font-size: 16px;
          text-align: justify;
          color: #555;
        }
        div {
          position: absolute;
          width: 100%;
          bottom: 10px;
          display: flex;
          flex-direction: row;
          flex-wrap: nowrap;
          justify-content: space-between;
          align-items: center;
          p {
            display: flex;
            flex-direction: row;
            flex-wrap: nowrap;
            justify-content: space-between;
            align-items: center;
            img {
              height: 16px;
            }
            span {
              font-size: 12px;
              color: #555;
              margin-left: 3px;
              margin-right: 3px;
            }
          }
          h4 {
            font-size: 12px;
            color: #888;
          }
        }
      }
      .new-right {
        margin-left: 10px;
        img {
          height: 68px;
        }
      }
    }
    .msg h2 {
      font-size: 18px;
      text-align: center;
      color: #666;
      padding-top: 58px;
    }
  }
}
</style>

附-3 下拉滚动追加显示数据

<template>
  <div class="news-box">
    <!--第七步:监听滚动事件-->
    <div class="scroll-container" ref="scrollContainer" @scroll.passive="handleScroll">
      <!--第十三步:我们需要在添加一个盒子 因为我们最上面和最下面的内容暂时是由padding撑开的-->
      <div :style="{
        paddingTop:topBlankFill+'px',
        paddingBotom:bottomBlankFill+'px'
      }">
        <div v-for="(item, index) in showDataList" :key="index">
          <router-link class="one-new" :to=" '/article/' + item.title + '/'+ item.reads + '/'+ item.from + '/'+ item.date + '/'+ item.image ">
            <!-- 新闻左侧标题、评论、来源部分 -->
            <div class="new-left">
              <h3>【{{index}}】{{ item.title }}</h3>
              <div>
                <p>
                  <img src="../assets/icons/msg.png" alt="评" />
                  <span>{{ item.reads }}</span>
                  <span>{{ item.from }}</span>
                </p>
                <h4>{{ item.date }}</h4>
              </div>
            </div>
            <!-- 新闻右侧图片部分 -->
            <div class="new-right">
              <!-- <img :src="imgsList[oneItem.image]" alt="PIC" /> -->
            </div>
          </router-link>
        </div>
      </div>

    </div>
  </div>
</template>

<script>
// 引入准备好的新闻图片相关信息
import imgsList from "../components/newsImgs.js";
import { setTimeout, clearTimeout } from "timers";
export default {
  data() {
    return {
      listData: [],
      oneItemHeight: 100, //2.第二步:每一个的高度border + padding +width
      containerHeight: 0,
      maxContainerSize: 0, //屏幕中可以显示的最大的数据条数
      startIndex: 0
      // topBlankFill: 50,
      // bottomBlankFill: 20
      // // endIndex: 0,
    };
  },
  created() {},
  async mounted() {
    this.getContainSize();
    this.listData = await this.getListData(20);
    console.log(this.$refs.scrollContainer.scrollTop);
  },
  computed: {
    endIndex() {
      let endIndex = this.startIndex + this.maxContainerSize; //9第九步:要注意的是容器的最下面可能是空白的,此时需要我们做一次判断
      if (endIndex > this.listData.length) {
        //考虑特殊情况:最后一屏的数据不满整个屏幕的时候
        endIndex = this.listData.length - 1; //这个时候已经滚动到最底部了
      }
      console.log(endIndex, "========endIndex=========");
      console.log(this.maxContainerSize, "========maxContainerSize=========");
      return endIndex;
    },
    //10第十步:定义一个带显示的数组列表元素,从获取的数组里面截取
    showDataList() {
      return this.listData.slice(this.startIndex, this.endIndex);
    },

    //11第十一步:需要定义一个上空白的高度
    topBlankFill() {
      return this.startIndex * this.oneItemHeight;
    },

    //12第十二步:需要定义上空白的高度
    bottomBlankFill() {
      return (this.listData.length - this.endIndex) * this.oneItemHeight;
    }
  },
  watch: {},
  methods: {
    getListData(num) {
      return this.$axios
        .get("http://localhost:4000/data?num=" + num)
        .then(res => {
          return res.data.list;
        })
        .catch(() => {
          this.msg = "请求失败稍后请重试...";
          return false;
        });
    },
    getContainSize() {
      //1.第一步:计算容器盒子的高度
      let height = this.$refs.scrollContainer.offsetHeight;
      //3.第三步:整个外面盒子的高度 / 每一个的高度 = 每一屏可以加载的最大条数
      let num = height / this.oneItemHeight;
      //4.第四步:要注意:如果上面露出了一点点,下面漏出来一点点  那么当前的数量就是上面出得整数+2
      let maxNum = Math.floor(num) + 2;

      this.maxContainerSize = maxNum;
      console.log(this.maxContainerSize);
    },
    //计算滚出去的数据
    setStartIndex() {
      let scrollTop = this.$refs.scrollContainer.scrollTop; //当前滚出去的高度
      console.log(this.$refs.scrollContainer.scrollTop, "scrollTop");
      let currentIndex = Math.floor(
        this.$refs.scrollContainer.scrollTop / this.oneItemHeight //滚出去的数据条数
      );
      if (currentIndex == this.startIndex) {
        return;
      }
      this.startIndex = currentIndex;
      console.log(this.startIndex, "===startIndex====");
    },
    //监听滚动方法的时候,计算startIndex的值
    async handleScroll() {
      this.setStartIndex();
      //第十四步:需要实现下拉加载分页功能:什么时候触发下拉加载功能呢,startIndex + contentSize > data.length-1
      console.log(
        this.startIndex,
        this.maxContainerSize,
        this.showDataList.length
      );
      if (
        this.startIndex + this.maxContainerSize >=
        this.listData.length - 1
      ) {
        console.log("滚动到了底部");
        //就追加数据
        let request = await this.getListData(20);

        this.listData = [...request, ...this.listData];
        console.log(this.listData);
      }
    }
  }
};
</script>
<style lang="scss" scoped>
.news-box {
  width: 100%;
  max-width: 800px;
  height: 100%;
  .scroll-container {
    width: 100%;
    height: 100%;
    overflow-y: auto;
    .one-new {
      text-decoration: none;
      display: block;
      display: flex;
      flex-direction: row;
      flex-wrap: nowrap;
      justify-content: space-between;
      border-bottom: 1px solid #ddd;
      padding: 14px 10px 5px;
      .new-left {
        height: 80px;
        position: relative;
        h3 {
          padding: 0;
          margin: 0;
          font-size: 16px;
          text-align: justify;
          color: #555;
        }
        div {
          position: absolute;
          width: 100%;
          bottom: 10px;
          display: flex;
          flex-direction: row;
          flex-wrap: nowrap;
          justify-content: space-between;
          align-items: center;
          p {
            display: flex;
            flex-direction: row;
            flex-wrap: nowrap;
            justify-content: space-between;
            align-items: center;
            img {
              height: 16px;
            }
            span {
              font-size: 12px;
              color: #555;
              margin-left: 3px;
              margin-right: 3px;
            }
          }
          h4 {
            font-size: 12px;
            color: #888;
          }
        }
      }
      .new-right {
        margin-left: 10px;
        img {
          height: 68px;
        }
      }
    }
    .msg h2 {
      font-size: 18px;
      text-align: center;
      color: #666;
      padding-top: 58px;
    }
  }
}
</style>

附-4

<template>
  <div class="news-box">
    <!--第七步:监听滚动事件-->
    <div class="scroll-container" ref="scrollContainer" @scroll.passive="handleScroll">
      <!--第十三步:我们需要在添加一个盒子 因为我们最上面和最下面的内容暂时是由padding撑开的-->
      <div :style="{
        paddingTop:topBlankFill+'px',
        paddingBotom:bottomBlankFill+'px'
      }">
        <div v-for="(item, index) in showDataList" :key="index">
          <router-link class="one-new" :to=" '/article/' + item.title + '/'+ item.reads + '/'+ item.from + '/'+ item.date + '/'+ item.image ">
            <!-- 新闻左侧标题、评论、来源部分 -->
            <div class="new-left">
              <h3>【{{index}}】{{ item.title }}</h3>
              <div>
                <p>
                  <img src="../assets/icons/msg.png" alt="评" />
                  <span>{{ item.reads }}</span>
                  <span>{{ item.from }}</span>
                </p>
                <h4>{{ item.date }}</h4>
              </div>
            </div>
            <!-- 新闻右侧图片部分 -->
            <div class="new-right">
              <!-- <img :src="imgsList[oneItem.image]" alt="PIC" /> -->
            </div>
          </router-link>
        </div>
      </div>

    </div>
  </div>
</template>

<script>
// 引入准备好的新闻图片相关信息
import imgsList from "../components/newsImgs.js";
import { setTimeout, clearTimeout } from "timers";
export default {
  data() {
    return {
      listData: [],
      oneItemHeight: 100, //2.第二步:每一个的高度border + padding +width
      containerHeight: 0,
      maxContainerSize: 0, //屏幕中可以显示的最大的数据条数
      startIndex: 0,
      isScrollStatus: null,
      isRequestStatus: true
      // topBlankFill: 50,
      // bottomBlankFill: 20
      // // endIndex: 0,
    };
  },
  created() {},
  async mounted() {
    this.getContainSize();
    this.listData = await this.getListData(20);
    if (!!this.listData && this.listData.length > 0) {
      this.listData = [...this.listData];
      this.isRequestStatus = false;
    }
  },
  computed: {
    endIndex() {
      let endIndex = this.startIndex + this.maxContainerSize * 2; //9第九步:要注意的是容器的最下面可能是空白的,此时需要我们做一次判断
      if (endIndex > this.listData.length) {
        //考虑特殊情况:最后一屏的数据不满整个屏幕的时候
        endIndex = this.listData.length - 1; //这个时候已经滚动到最底部了
      }
      return endIndex;
    },
    //10第十步:定义一个带显示的数组列表元素,从获取的数组里面截取
    showDataList() {
      let startIndex = 0;
      let endIndex = 0;
      if (this.startIndex <= this.maxContainerSize) {
        //这个时候上面还没有滚出去
        startIndex = 0;
      } else {
        startIndex = this.startIndex - this.maxContainerSize * 2; //需要减去滚两屏的数据
      }

      return this.listData.slice(startIndex, this.endIndex);
    },

    //11第十一步:需要定义一个上空白的高度
    topBlankFill() {
      //20.第二十步:上下的padding也需要
      //第十九步:设置前后缓冲区
      let paddingTop = 0;
      if (this.startIndex > this.maxContainerSize) {
        //已经滚出去一屏
        // 当当前滚动位置大于屏幕容积后才填充空白
        paddingTop =
          (this.startIndex - this.maxContainerSize) * this.oneItemHeight;
        console.log(this.startIndex, this.maxContainerSize,paddingTop);
      }
      return paddingTop;
    },

    //12第十二步:需要定义上空白的高度
    bottomBlankFill() {
      return (this.listData.length - this.endIndex) * this.oneItemHeight;
    }
  },
  watch: {},
  methods: {
    getListData(num) {
      return this.$axios
        .get("http://localhost:4000/data?num=" + num)
        .then(res => {
          return res.data.list;
        })
        .catch(() => {
          this.msg = "请求失败稍后请重试...";
          return false;
        });
    },

    getContainSize() {
      //1.第一步:计算容器盒子的高度
      let height = this.$refs.scrollContainer.offsetHeight;
      //3.第三步:整个外面盒子的高度 / 每一个的高度 = 每一屏可以加载的最大条数
      let num = height / this.oneItemHeight;
      //4.第四步:要注意:如果上面露出了一点点,下面漏出来一点点  那么当前的数量就是上面出得整数+2
      let maxNum = Math.floor(num) + 2;

      this.maxContainerSize = maxNum;
    },
    //计算滚出去的数据
    setStartIndex() {
      let scrollTop = this.$refs.scrollContainer.scrollTop; //当前滚出去的高度
      let currentIndex = Math.floor(
        this.$refs.scrollContainer.scrollTop / this.oneItemHeight //滚出去的数据条数
      );
      if (currentIndex == this.startIndex) {
        return;
      }
      this.startIndex = currentIndex;
    },
    //监听滚动方法的时候,计算startIndex的值
    handleScroll() {
      // this.debounce(this.scrollFun, 500);
      this.requestF();
      // this.scrollFun();
    },

    requestF() {
      let requestAnimationFrame =
        window.requestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.msRequestAnimationFrame;
      //浏览器防抖优化:根据浏览器FPS采用递归方法,队列调用requestAnimationFrame方法实现优化
      let fps = 30;
      let interval = 1000 / fps;
      let then = Date.now();
      requestAnimationFrame(() => {
        let now = Date.now();
        let delta = now - then;
        then = now;
        this.scrollFun();
        if (delta >= interval) {
          requestAnimationFrame(arguments.callee);
        }
      });
    },

    debounce(fn, delay) {
      clearTimeout(this.isScrollStatus);
      this.isScrollStatus = setTimeout(function() {
        fn();
      }, delay);
    },
    async scrollFun() {
      console.log("滚动了");
      this.setStartIndex();
      if (this.startIndex + this.maxContainerSize >= this.listData.length - 1) {
        //就追加数据
        let request = await this.getListData(20);
        this.listData = [...request, ...this.listData];
      }
    }
  }
};
</script>

<style lang="scss" scoped>
.news-box {
  width: 100%;
  max-width: 800px;
  height: 100%;
  .scroll-container {
    width: 100%;
    height: 100%;
    overflow-y: auto;
    .one-new {
      text-decoration: none;
      display: block;
      display: flex;
      flex-direction: row;
      flex-wrap: nowrap;
      justify-content: space-between;
      border-bottom: 1px solid #ddd;
      padding: 14px 10px 5px;
      .new-left {
        height: 80px;
        position: relative;
        h3 {
          padding: 0;
          margin: 0;
          font-size: 16px;
          text-align: justify;
          color: #555;
        }
        div {
          position: absolute;
          width: 100%;
          bottom: 10px;
          display: flex;
          flex-direction: row;
          flex-wrap: nowrap;
          justify-content: space-between;
          align-items: center;
          p {
            display: flex;
            flex-direction: row;
            flex-wrap: nowrap;
            justify-content: space-between;
            align-items: center;
            img {
              height: 16px;
            }
            span {
              font-size: 12px;
              color: #555;
              margin-left: 3px;
              margin-right: 3px;
            }
          }
          h4 {
            font-size: 12px;
            color: #888;
          }
        }
      }
      .new-right {
        margin-left: 10px;
        img {
          height: 68px;
        }
      }
    }
    .msg h2 {
      font-size: 18px;
      text-align: center;
      color: #666;
      padding-top: 58px;
    }
  }
}
</style>