vue2 与 vue3 虚拟列表实现

我下列的所有代码都在该 Gitee 仓库中:
https://gitee.com/ls1551724864/vue2-3-virtual-scroll-list

1、概述

  1. 一般长列表用在后端传递大量数据,要求前端进行展示的情况

  2. 先来看看原生的一个加载大量数据的一个情况:

    <div id="container"></div>
    
    <script>
      // 演示大量 DOM 加载时的缓慢问题,也就是“长列表问题”
      const container = document.getElementById("container");
    
      for (let i = 0; i < 100000; i++) {
        const div = document.createElement("div")
        div.innerText = i
        container.appendChild(div)
      }
    </script>
    

    在这里插入图片描述

  3. 页面初次渲染完毕后,一旦后续发生DOM结构的变化,会出现重排和重绘、情况;绘制类的工作由浏览器的GUI渲染引擎执行,而JavaScript代码则是由JS引擎执行;由于渲染的机制,如果页面中存在大量的DOM渲染,可能导致网页出现“失去响应”的假象(白屏渲染)

2、计时

  1. 现在我使用console.time()来进行一个代码执行时间的打印:

    <div id="container"></div>
    
    <script>
      // 演示大量 DOM 加载时的缓慢问题,也就是“长列表问题”
      const container = document.getElementById("container");
    
      console.time('长列表')
      for (let i = 0; i < 100000; i++) {
        const div = document.createElement("div")
        div.innerText = i
        container.appendChild(div)
      }
      console.timeEnd('长列表')
    </script>
    

    补充:console.time()console.timeEnd()是用来计算两个包裹的中间代码的执行时间,但是参数是对应起来的,只会计算这两个方法中参数一样的中间代码部分的执行时间。

  2. 来看一下页面打印结果:

    在这里插入图片描述

  3. 可以明显看出,打印出的结果是0.1s的时间,但是浏览器页面加载明显时间更长,所以注意,console.time() 打印的是JavaScript代码执行的时间,并不是页面渲染的时间

3、JavaScript线程

  1. JavaScript是不是单线程的?

    • 是单线程的,但是指的是JavaScript的主线程只有一个
  2. 常见的线程:

    • JS引擎线程
    • GUI渲染线程
    • 事件触发器线程
    • 时间触发器线程
    • 网络请求线程
    • Event Loop线程
  3. 所以想要查看页面渲染的时间,可以利用Event Loop的宏任务与微任务的原理来操作:

    <div id="container"></div>
    
    <script>
      // 演示大量 DOM 加载时的缓慢问题,也就是“长列表问题”
      const container = document.getElementById("container");
    
      console.time('长列表')
      for (let i = 0; i < 100000; i++) {
        const div = document.createElement("div")
        div.innerText = i
        container.appendChild(div)
      }
      setTimeout(() => {
        console.timeEnd('长列表')
      }, 0);
    </script>
    

    在这里插入图片描述

  4. 上面我将console.timeEnd()用定时器进行了包裹,所以让其代码执行进行了阻塞,所以打印出来的结果就是页面渲染的事件,可以看出时间明显比JavaScript代码执行长了很多

4、分片加载

  1. 分片加载的逻辑就是,设计一个计数器,每次做一个循环先渲染几百条几千条数据,然后让计数器进行累加,然后进行递归调用渲染数据的函数;当达到了目标数据条数的时候,就不再去执行该函数;其中对重要的一点:每次进行递归调用数据加载函数的时候,需要将其加入到宏任务队列当中,这样才不会造成渲染线程的阻塞,用户体验很好

  2. 看一下具体的代码:

    <div id="container"></div>
    
    <script>
      const container = document.getElementById("container");
    
      // 1、记录加载到的位置
      let index = 0
    
      // 2、每次加载 500 条数据,一共加载 50w 条数据,一共加载 1000 次
      // 封装一个加载数据的函数
      function loadData() {
        // 当 index 计数器大于了 50w,那么说明数据加载完毕,就不在进行页面的渲染
        if (index >= 500000) return
    
        // 使用 for 循环每次加载 500 条数据
        for (let i = 0; i < 500; i++) {
          const div = document.createElement('div')
          div.innerHTML = i + index
          container.appendChild(div)
        }
    
        // 让计数器 index + 500
        index += 500
    
        // 讲下一次递归调用,放在下一个宏任务中去执行
        setTimeout(loadData, 0)
      }
    
      // 调用加载数据的函数
      console.time('分片加载')
      loadData()
      setTimeout(() => {
        console.timeEnd('分片加载')
      }, 0);
    </script>
    
  3. 看一下页面效果:

    在这里插入图片描述

  4. 从上图就可以看出,现在渲染50w条数据的时间明显少了很多,但是还存在一个缺陷:可以发现我每次拖动滚动条的时候,滚动条还在进行上移,说明后面的数据还在进行加载,这个当然对用户体验是很好的,但是对于浏览器来说,要创建50w个相同结构的DOM,这对于浏览器的性能来说还是影响比较大的,其实每次就让他加载固定个数的DOM节点即可,不要把50w条全部加载出来。

5、vue-virtual-scroll-list

  1. 这个插件就是vue中的一个长列表的插件,官网地址:https://tangbc.github.io/vue-virtual-scroll-list/#/

  2. 来看一下该组件的渲染情况:

    在这里插入图片描述

  3. 可以明显看出,其渲染的时候,DOM节点数量都是固定的,并不会将所有的内容全部加载出来

6、自己实现vue虚拟列表

① vue2

Ⅰ. 项目搭建

  1. 建一个新的文件夹,在这个文件夹中创建一个vue2的项目:vue create vue2-virtual-scroll,模板选择默认的vue2模板即可;

  2. 在components目录下创建一个List.vue组件,用来进行虚拟列表的展示;

  3. 在App.vue主入口页面中去引入该组件:

    <template>
      <div id="app">
        <List
          :items="items"
          :size="60"
          :shownumber="10"
        />
      </div>
    </template>
    
    <script>
    import List from './components/List.vue'
    
    export default {
      name: 'App',
      components: {
        List
      },
      computed: {
        // 要进行渲染的数据列表
        items () {
          // 自己模拟一万条数据,将其内容进行填充
          return Array(10000).fill('').map((item, index) => ({
            id: index,
            content: '列表项内容' + index
          }))
        }
      }
    }
    </script>
    
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    </style>
    

    注意:

    • 可以发现我的List组件上面有几个参数,分别介绍一下这几个参数的意义:
      1. items:要进行渲染的列表数据;
      2. size:每一条数据的高度;
      3. showNumber:每次渲染的数据条数(DOM个数);
      • 后续还可以继续给这个组件添加属性,用来决定一些数据的性质等
    • 因为没有真实的数据,我在computed计算属性中,通过数组遍历的方式创建了一万条假数据,并且都填充上了值,让这数组中的值充当数据;
  4. 先把List.vue虚拟列表页面组件搭建起来:

    <template>
      <div
        class="container"
        :style="{ height: containerHeight }"
      >
        <!-- 数据列表 -->
        <div class="list">
          <!-- 列表项 -->
          <div
            v-for="item in showData"
            :key="item.id"
            :style="{ height: size + 'px' }"
          >
            {{ item.content }}
          </div>
    
          <!-- 用于撑开高度的元素 -->
          <div
            class="bar"
            :style="{ height: barHeight }"
          />
        </div>
      </div>
    </template>
    
    <script>
    export default {
      name: 'VircualList',
      props: {
        // 要渲染的数据
        items: {
          type: Array,
          required: true
        },
        // 每条数据渲染的节点的高度
        size: {
          type: Number,
          required: true
        },
        // 每次渲染的 DOM 节点个数
        shownumber: {
          type: Number,
          required: true
        }
      },
      data () {
        return {
          start: 0, // 要展示的数据的起始下标
          end: this.shownumber // 要展示的数据的结束下标
        }
      },
      computed: {
        // 最终筛选出的要展示的数据
        showData () {
          return this.items.slice(this.start, this.end)
        },
        // 容器的高度
        containerHeight () {
          return this.size * this.shownumber + 'px'
        },
        // 撑开容器内容高度的元素的高度
        barHeight () {
          return this.size * this.items.length + 'px'
        }
      }
    }
    </script>
    
    <style scoped>
    .container {
      overflow-y: scroll;
      background-color: rgb(150, 195, 238);
      font-size: 20px;
      font-weight: bold;
      line-height: 60px;
    }
    </style>
    

    注意几点:

    1. 接收父组件传递过来的数据,然后我声明了两个变量,startend,这两个就是为了每次进行渲染要显示的数据,在items数组中的起始结束下标位置;这两的长度固定在shownumber个单位以内;
    2. 可以发现我对container容器设置了一个高度,因为在真实的开发中,一般就是在一块区域中进行展示列表数据,所以把这个模拟成一个页面的小框框区域,我设置的其高度就是shownumber个列表项的高度,刚好让shownumber个数据完全展示出来;
    3. 我在页面中还创建了一个类名为bar的div节点,这个是为了撑开整个容器的高度,让其有一个滚动的区域,高度就是整个items数据的长度×每个列表项的高度size
  5. 先来看页面的效果:

    在这里插入图片描述

Ⅱ. 虚拟列表制作

  1. 给容器绑定一个滚动事件,当容器发生滚动的时候,就让其动态的去渲染后续的数据

    <template>
      <div
        class="container"
        :style="{ height: containerHeight }"
        @scroll="handleScroll"
        ref="container"
      >
        <!-- 数据列表 -->
        <div class="list">
          <!-- 列表项 -->
          <div
            v-for="item in showData"
            :key="item.id"
            :style="{ height: size + 'px' }"
          >
            {{ item.content }}
          </div>
    
          <!-- 用于撑开高度的元素 -->
          <div
            class="bar"
            :style="{ height: barHeight }"
          />
        </div>
      </div>
    </template>
    
    <script>
    export default {
      name: 'VircualList',
      props: {
        // 要渲染的数据
        items: {
          type: Array,
          required: true
        },
        // 每条数据渲染的节点的高度
        size: {
          type: Number,
          required: true
        },
        // 每次渲染的 DOM 节点个数
        shownumber: {
          type: Number,
          required: true
        }
      },
      data () {
        return {
          start: 0, // 要展示的数据的起始下标
          end: this.shownumber // 要展示的数据的结束下标
        }
      },
      computed: {
        // 最终筛选出的要展示的数据
        showData () {
          return this.items.slice(this.start, this.end)
        },
        // 容器的高度
        containerHeight () {
          return this.size * this.shownumber + 'px'
        },
        // 撑开容器内容高度的元素的高度
        barHeight () {
          return this.size * this.items.length + 'px'
        }
      },
      methods: {
        // 容器的滚动事件
        handleScroll () {
          // 获取容器顶部滚动的尺寸
          const scrollTop = this.$refs.container.scrollTop
    
          // 计算卷去的数据条数,用计算的结果作为获取数据的起始和结束下标
          // 起始的下标就是卷去的数据条数,向下取整
          this.start = Math.floor(scrollTop / this.size)
          // 结束的下标就是起始的下标加上要展示的数据条数
          this.end = this.start + this.shownumber
        }
      }
    }
    </script>
    
    <style scoped>
    .container {
      overflow-y: scroll;
      background-color: rgb(150, 195, 238);
      font-size: 20px;
      font-weight: bold;
      line-height: 60px;
    }
    </style>
    

    在这里插入图片描述

    注意:

    • 在每次滚动的时候,就需要去修改要重新渲染的数据的起始和结束下标:
      • 起始下标的计算 = 区域向上卷去的高度 scrollTop ÷每个数据的高度 size ,然后向下取整
      • 结束下标的计算 = 起始的下标 + 页面展示的数据的条数 shownumber
  2. 可以发现上图中,数据发生了变化,但是列表还是依旧向上滚动,接下来需要给列表做定位的处理,只需要每次滚动的时候,让列表跟着向下滚动即可

    <template>
      <div
        class="container"
        :style="{ height: containerHeight }"
        @scroll="handleScroll"
        ref="container"
      >
        <!-- 数据列表 -->
        <div
          class="list"
          :style="{ top: listTop }"
        >
          <!-- 列表项 -->
          <div
            v-for="item in showData"
            :key="item.id"
            :style="{ height: size + 'px' }"
          >
            {{ item.content }}
          </div>
    
          <!-- 用于撑开高度的元素 -->
          <div
            class="bar"
            :style="{ height: barHeight }"
          />
        </div>
      </div>
    </template>
    
    <script>
    export default {
      name: 'VircualList',
      props: {
        // 要渲染的数据
        items: {
          type: Array,
          required: true
        },
        // 每条数据渲染的节点的高度
        size: {
          type: Number,
          required: true
        },
        // 每次渲染的 DOM 节点个数
        shownumber: {
          type: Number,
          required: true
        }
      },
      data () {
        return {
          start: 0, // 要展示的数据的起始下标
          end: this.shownumber // 要展示的数据的结束下标
        }
      },
      computed: {
        // 最终筛选出的要展示的数据
        showData () {
          return this.items.slice(this.start, this.end)
        },
        // 容器的高度
        containerHeight () {
          return this.size * this.shownumber + 'px'
        },
        // 撑开容器内容高度的元素的高度
        barHeight () {
          return this.size * this.items.length + 'px'
        },
        // 列表向上滚动时要动态改变 top 值
        listTop () {
          return this.start * this.size + 'px'
        }
      },
      methods: {
        // 容器的滚动事件
        handleScroll () {
          // 获取容器顶部滚动的尺寸
          const scrollTop = this.$refs.container.scrollTop
    
          // 计算卷去的数据条数,用计算的结果作为获取数据的起始和结束下标
          // 起始的下标就是卷去的数据条数,向下取整
          this.start = Math.floor(scrollTop / this.size)
          // 结束的下标就是起始的下标加上要展示的数据条数
          this.end = this.start + this.shownumber
        }
      }
    }
    </script>
    
    <style scoped>
    .container {
      position: relative;
      overflow-y: scroll;
      background-color: rgb(150, 195, 238);
      font-size: 20px;
      font-weight: bold;
      line-height: 60px;
      text-align: center;
    }
    
    .list {
      position: absolute;
      top: 0;
      width: 100%;
    }
    </style>
    

    注意:列表动态的高度top是当前页面渲染的数据的起始下标 × 每个数据的高度, 即卷上去的列表高度

  3. 来看现在的页面效果:

    在这里插入图片描述

  4. 上面就可以很清楚的看出列表项似乎是一直在向下滚动的,但是页面的DOM节点数一直没有改变。

② vue3

Ⅰ. 项目搭建

  1. 使用vite搭建项目:npm init vite@latest
  2. 项目命名:vue3-virtual-scroll
  3. 然后进入到该项目中,需要安装依赖:npm install
  4. 运行项目:npm run dev
  5. 后面的配置选择vue相关的基础配置即可

Ⅱ. 虚拟列表制作

  1. 这里不再很详细的说明了,其逻辑与上述vue2的制作过程一样,知识语法不一样而已,我只将最重要的两个页面的代码贴出来:App.vue和List.vue

  2. App.vue页面中的代码:

    <template>
      <div id="app">
        <List
          :items="items"
          :size="60"
          :shownumber="10"
        />
      </div>
    </template>
    
    <script setup>
    // ------------------------------------- 导入模块 ----------------------------------
    // 导入 vue3 的 API
    import { computed } from 'vue'
    // 导入列表组件
    import List from './components/List.vue'
    
    // ------------------------------------- 声明数据 ----------------------------------
    // 模拟要进行渲染的数据列表
    const items = computed(() => {
      // 自己模拟一万条数据,将其内容进行填充
      return Array(10000).fill('').map((item, index) => ({
        id: index,
        content: '列表项内容' + index
      }))
    })
    </script>
    
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    </style>
    
  3. List.vue页面中的代码:

    <template>
      <div
        class="container"
        :style="{ height: containerHeight }"
        @scroll="handleScroll"
        ref="container"
      >
        <!-- 数据列表 -->
        <div
          class="list"
          :style="{ top: listTop }"
        >
          <!-- 列表项 -->
          <div
            v-for="item in showData"
            :key="item.id"
            :style="{ height: size + 'px' }"
          >
            {{ item.content }}
          </div>
    
          <!-- 用于撑开高度的元素 -->
          <div
            class="bar"
            :style="{ height: barHeight }"
          />
        </div>
      </div>
    </template>
    
    <script setup>
    // ------------------------------------- 导入模块 ----------------------------------
    // 导入 vue3 的 API
    import { ref, toRefs, computed } from 'vue'
    
    // ------------------------------------- 组件传值 ----------------------------------
    // 接收父组件传递的数据
    const props = defineProps({
      // 要渲染的数据
      items: {
        type: Array,
        required: true
      },
      // 每条数据渲染的节点的高度
      size: {
        type: Number,
        required: true
      },
      // 每次渲染的 DOM 节点个数
      shownumber: {
        type: Number,
        required: true
      }
    })
    // 使用 toRefs 包裹 props,让解构获得的父组件传递的参数变为响应式的
    const { items, size, shownumber } = toRefs(props)
    
    // ------------------------------------- 声明变量 ----------------------------------
    const container = ref(null) // 页面 container 节点
    let start = ref(0) // 要展示的数据的起始下标
    let end = ref(shownumber.value) // 要展示的数据的结束下标
    
    // ------------------------------------- 计算属性 ----------------------------------
    const showData = computed(() => items.value.slice(start.value, end.value)) // 最终筛选出的要展示的数据
    const containerHeight = computed(() => size.value * shownumber.value + 'px') // 容器的高度
    const barHeight = computed(() => size.value * items.value.length + 'px') // 撑开容器内容高度的元素的高度
    const listTop = computed(() => start.value * size.value + 'px') // 列表向上滚动时要动态改变 top 值
    
    // ------------------------------------- 声明函数 ----------------------------------
    // 容器的滚动事件
    const handleScroll = () => {
      // 获取容器顶部滚动的尺寸
      const scrollTop = container.value.scrollTop
    
      // 计算卷去的数据条数,用计算的结果作为获取数据的起始和结束下标
      // 起始的下标就是卷去的数据条数,向下取整
      start.value = Math.floor(scrollTop / size.value)
      // 结束的下标就是起始的下标加上要展示的数据条数
      end.value = start.value + shownumber.value
    }
    </script>
    
    <style scoped>
    .container {
      position: relative;
      overflow-y: scroll;
      background-color: rgb(150, 195, 238);
      font-size: 20px;
      font-weight: bold;
      line-height: 60px;
      text-align: center;
    }
    
    .list {
      position: absolute;
      top: 0;
      width: 100%;
    }
    </style>
    
  • 10
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
Vue 3提供了一个新的组件API,使得构建虚拟列表变得更加容易。下面是一个简单的实现示例: 1. 安装依赖: ```bash npm install vue@next npm install vue3-virtual-scroll-list ``` 2. 在组件中使用虚拟列表: ```vue <template> <virtual-scroll-list :size="50" :remain="20" :data-key="'id'" :data-sources="items" @scroll="handleScroll" > <template v-slot="{ data }"> <div v-for="item in data" :key="item.id">{{ item.text }}</div> </template> </virtual-scroll-list> </template> <script> import { ref } from 'vue' import VirtualScrollList from 'vue3-virtual-scroll-list' export default { components: { VirtualScrollList, }, setup() { const items = ref([]) // 初始化数据 for (let i = 0; i < 10000; i++) { items.value.push({ id: i, text: `Item ${i}`, }) } const handleScroll = (scrollTop) => { // 处理滚动事件 console.log(scrollTop) } return { items, handleScroll, } }, } </script> ``` 在这个示例中,我们使用 `vue3-virtual-scroll-list` 组件来实现虚拟列表。这个组件需要传入一些参数,包括: - `size`:每个项的高度 - `remain`:上下额外渲染项的数量 - `data-key`:数据中每个项的唯一标识符 - `data-sources`:数据源 - `scroll`:滚动事件的回调函数 在模板中,我们使用插槽来渲染每个项。同时,组件还会将已经渲染的项缓存起来,以提高性能。 在 `setup` 函数中,我们初始化了一个 `items` 的响应式变量,并将它传入 `data-sources` 中。我们还定义了一个 `handleScroll` 函数来处理滚动事件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凉爽爽爽爽爽爽爽爽爽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值